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

如何使用AVQueuePlayer播放歌曲?多曲目播放问题求助

问题:AVQueueManager实现单首/专辑播放的疑难

找不到易懂的AVQueuePlayer文档或示例,正尝试将原单轨播放的AudioManager改造为支持单首/整张专辑多曲目播放的版本,需要AVQueuePlayer的代码示例或清晰实现思路。

原单轨播放代码(AVAudioPlayer)

final class AudioManager: ObservableObject {
    var player: AVAudioPlayer?
    @Published var isPlaying: Bool = false
    
    func startPlayer(track: String) {
        guard let url = Bundle.main.url(forResource: track, withExtension: "mp3") else {return}
        
        do {
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
            player = try AVAudioPlayer(contentsOf: url)
            player?.play()
            isPlaying = true
        } catch {
            print("Failed to initialize player: ", error)
        }
    }
    
    func playPause() {
        guard let player = player else {
            print("Instance of audio player not found")
            return
        }
        if player.isPlaying {
            player.pause()
            isPlaying = false
        } else {
            player.play()
            isPlaying = true
        }
    }
    
    func stop() {
        guard let player = player else {
            return
        }
        if player.isPlaying {
            player.stop()
            isPlaying = false
        }
    }
}

尝试编写的AVQueueManager代码

import Foundation
import AVKit

final class AudioQueueManager: ObservableObject {
    var player: AVQueuePlayer?
    @Published var isPlaying: Bool = false
    @Published var someType: SomeType = .album(Album.data)
    
    func startPlayer(someType: SomeType) {
        switch someType {
        case .song(let song):
            let url = Bundle.main.url(forResource: song.track, withExtension: "mp3")
            do {
                try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
                try AVAudioSession.sharedInstance().setActive(true)
                player = AVQueuePlayer(url: url!)
                player?.play()
                isPlaying = true
            } catch {
                print("Failed to initialize player: ", error)
            }
        case .album(let album):
            var items: [AVPlayerItem] = []
            album.songs?.forEach({ albumsong in
                items.append(AVPlayerItem(url: Bundle.main.url(forResource: albumsong.track, withExtension: "mp3")!))
            })
            do {
                try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
                try AVAudioSession.sharedInstance().setActive(true)
                player = AVQueuePlayer(items: items)
                player?.play()
                isPlaying = true
            } catch {
                print("Failed to initialize player: ", error)
            }
        }
    }
    
    func playPause() {
        guard let player = player else {
            print("Instance of audio player not found")
            return
        }
        if player.timeControlStatus == .playing {
            player.pause()
            isPlaying = false
        } else {
            player.play()
            isPlaying = true
        }
    }
    
    func stop() {
        guard let player = player else {
            return
        }
            player.removeAllItems()
            isPlaying = false
    }
}

视图中启动播放器的代码

enum SomeType {
    case song(Song)
    case album(Album)
}
@EnvironmentObject var audioQueueManger: AudioQueueManager
switch type {
                case .song(let song):
                    audioQueueManager.stop()
                    audioQueueManager.isPlaying = true
                    audioQueueManager.startPlayer(someType: .song(song))
                    
                    musicPlayerVm.addCurrentSong()
                case .album(let album):
                    audioQueueManager.stop()
                    audioQueueManager.isPlaying = true
                    audioQueueManager.startPlayer(someType: .album(album))
                }

遇到的具体问题

  • 停止/启动新歌曲/专辑功能无效,找不到AVQueuePlayer对应player.stop()的方法
  • 快进10秒无法像AVAudioPlayer那样用currentTime += 10实现,尝试player?.currentTime().value += 10但value不可变,且currentTime需要CMTime类型
  • 单首歌曲播放正常,但播放专辑时切换到另一专辑会报错:An AVPlayerItem cannot be associated with more than one instance of AVPlayer

解决方案

1. 修复停止功能与AVPlayerItem重复关联问题

AVQueuePlayer没有直接的stop方法,需销毁旧播放器实例彻底清理状态,避免AVPlayerItem被多个播放器关联:

func stop() {
    player?.pause()
    player?.removeAllItems()
    player = nil // 关键:销毁旧实例,断绝item关联
    isPlaying = false
    // 可选:停用音频会话释放资源
    do {
        try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
    } catch {
        print("Failed to deactivate audio session: \(error)")
    }
}

// 优化startPlayer:先停止旧播放,统一处理播放项
init() {
    // 初始化时一次性设置音频会话
    do {
        try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
    } catch {
        print("Failed to set audio session category: \(error)")
    }
}

func startPlayer(someType: SomeType) {
    stop() // 先清理旧播放
    
    var playerItems: [AVPlayerItem] = []
    switch someType {
    case .song(let song):
        guard let url = Bundle.main.url(forResource: song.track, withExtension: "mp3") else {
            print("Song URL not found: \(song.track)")
            return
        }
        playerItems.append(AVPlayerItem(url: url))
    case .album(let album):
        guard let songs = album.songs else {
            print("Album has no songs")
            return
        }
        for song in songs {
            guard let url = Bundle.main.url(forResource: song.track, withExtension: "mp3") else {
                print("Song URL not found: \(song.track)")
                continue
            }
            playerItems.append(AVPlayerItem(url: url))
        }
    }
    
    guard !playerItems.isEmpty else {
        print("No valid player items to play")
        return
    }
    
    do {
        try AVAudioSession.sharedInstance().setActive(true)
        player = AVQueuePlayer(items: playerItems)
        player?.play()
        isPlaying = true
    } catch {
        print("Failed to initialize player: \(error)")
    }
}

2. 实现快进10秒功能

AVQueuePlayer的currentTime为只读属性,需用seek(to:)方法结合CMTime计算目标位置:

func forward10Seconds() {
    guard let player = player, let currentItem = player.currentItem else { return }
    let currentTime = currentItem.currentTime()
    // 计算快进10秒后的CMTime
    let targetTime = CMTimeAdd(currentTime, CMTimeMakeWithSeconds(10, preferredTimescale: 1))
    // 不超过当前歌曲时长则跳转,否则自动切下一首
    if targetTime <= currentItem.duration {
        player.seek(to: targetTime)
    } else {
        player.advanceToNextItem()
    }
}

3. 优化播放状态同步

添加KVO监听播放器状态,避免手动设置isPlaying出现不一致:

final class AudioQueueManager: ObservableObject {
    // ... 原有属性 ...
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "timeControlStatus", let player = object as? AVQueuePlayer {
            DispatchQueue.main.async {
                self.isPlaying = player.timeControlStatus == .playing
            }
        }
    }
    
    func startPlayer(someType: SomeType) {
        // ... 原有逻辑 ...
        player?.addObserver(self, forKeyPath: "timeControlStatus", options: [.new], context: nil)
    }
    
    deinit {
        player?.removeObserver(self, forKeyPath: "timeControlStatus")
    }
}

添加监听后,无需手动在playPause或startPlayer中设置isPlaying,由播放器状态自动同步。


内容的提问来源于stack exchange,提问作者AdamLeet

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 11:55:25