SwiftUI中AVQueuePlayer切换页面崩溃:AVPlayerItem关联冲突
问题描述
我有一个用于展示视频序列的SwiftUI View组件,在代码中频繁使用该组件。当从展示此View的页面切换到另一个使用相同View但加载其他视频的页面时,发生崩溃,错误提示:
AVQueuePlayer. An AVPlayerItem cannot be associated with more than one instance of AVPlayer
以下是我的Swift代码:
import SwiftUI import AVKit import AVFoundation struct PlayerView<VideoOverlay: View>: View { @StateObject private var playerManager: QueuePlayerManager @ViewBuilder var videoOverlay: () -> VideoOverlay private let placeholder: String init( _ sequence: [String], endAction: PlayerEndAction = .none, @ViewBuilder videoOverlay: @escaping () -> VideoOverlay ) { _playerManager = StateObject( wrappedValue: QueuePlayerManager( sequence: sequence, endAction: endAction ) ) self.placeholder = sequence.first ?? .init() self.videoOverlay = videoOverlay } var body: some View { GeometryReader { geometry in if self.playerManager.isReadyToPlay { VideoPlayer( player: playerManager.player, videoOverlay: videoOverlay ) .aspectRatio(contentMode: .fill) .frame(width: geometry.size.height * (DeviceUtils.isMediumScreen ? 1.25 : 0.75), height: geometry.size.height) .position(x: geometry.size.width / 2, y: geometry.size.height / 2) .disabled(true) .clipped() .onAppear { self.playerManager.player.play() } } else { Image(placeholder) .resizable() .scaledToFill() .clipped() .transition(.opacity) } } .edgesIgnoringSafeArea(.all) } private class QueuePlayerManager: ObservableObject { let player: AVQueuePlayer private var isRemovedItems: Bool = false @Published private(set) var isReadyToPlay = false init( sequence: [String], endAction: PlayerEndAction ) { var sequence = sequence if let lastItem = sequence.last { sequence.append(lastItem) } let urls: [URL] = sequence.map { Bundle.main.url( forResource: $0, withExtension: "mp4" )! } let playerItems = urls.map { AVPlayerItem(url: $0) } player = AVQueuePlayer(items: playerItems) player.actionAtItemEnd = .none if endAction != .none { //MARK: - Add observer loop NotificationCenter.default.addObserver( forName: .AVPlayerItemDidPlayToEndTime, object: nil, queue: nil ) { [weak self] notification in guard let self = self else { return } let currentItem = notification.object as? AVPlayerItem //MARK: - Loop last item if endAction == .loop, let currentItem = currentItem { if self.isRemovedItems { self.player.seek(to: .zero) } self.player.advanceToNextItem() if self.player.canInsert(currentItem, after: nil) { self.player.insert(currentItem, after: nil) } if !self.isRemovedItems { self.player.seek(to: .zero) } } //MARK: - Remove other items guard !self.isRemovedItems else { return } let lastTwoItems = playerItems.suffix(2) playerItems.forEach { item in if !lastTwoItems.contains(item) { self.player.remove(item) } } self.isRemovedItems.toggle() } } NotificationCenter.default.addObserver( forName: .AVPlayerItemTimeJumped, object: player.currentItem, queue: .main ) { [weak self] _ in guard let self = self else { return } withAnimation { self.isReadyToPlay = true } } } deinit { NotificationCenter.default.removeObserver( self, name: .AVPlayerItemDidPlayToEndTime, object: nil ) NotificationCenter.default.removeObserver( self, name: .AVPlayerItemTimeJumped, object: nil ) } } } enum PlayerEndAction: Equatable { case none, loop } extension PlayerView where VideoOverlay == EmptyView { init( _ sequence: [String], endAction: PlayerEndAction ) { self.init( sequence, endAction: endAction ) { EmptyView() } } }
我尝试调整初始化参数,曾考虑创建新的AVPlayer实例,但未能实现。
解决方案
问题根源
崩溃核心原因:
- 页面切换时,旧
PlayerView的QueuePlayerManager未及时释放,若新页面视频序列包含旧页面的视频文件,会导致同一个AVPlayerItem被多个AVQueuePlayer实例关联。 - 通知观察者注册时使用
object: nil,会接收所有播放器的播放结束通知,可能误操作不属于当前管理器的AVPlayerItem,加剧冲突。
修复步骤
1. 强制释放播放器资源
在QueuePlayerManager的deinit方法中,除移除观察者外,清空播放器所有项目并暂停,确保AVPlayerItem能被正确回收:
deinit { NotificationCenter.default.removeObserver(self) player.removeAllItems() player.pause() }
2. 限制通知监听范围
注册AVPlayerItemDidPlayToEndTime通知时,指定当前播放器为监听对象,只处理当前播放器的事件:
NotificationCenter.default.addObserver( forName: .AVPlayerItemDidPlayToEndTime, object: player, // 仅监听当前播放器的项目结束通知 queue: .main ) { [weak self] notification in // 原有逻辑 }
3. 优化页面切换时的状态
在PlayerView的onDisappear回调中暂停播放器,加速资源释放:
var body: some View { GeometryReader { geometry in // 原有视图内容 } .edgesIgnoringSafeArea(.all) .onDisappear { playerManager.player.pause() } }
4. 修复循环逻辑的Item重复问题
原循环逻辑直接复用播放结束的currentItem,可能因旧实例未释放引发冲突,改为创建新的AVPlayerItem实例:
// 替换原循环部分代码 if endAction == .loop, let currentItem = currentItem { guard let urlAsset = currentItem.asset as? AVURLAsset else { return } let newLoopItem = AVPlayerItem(url: urlAsset.url) player.insert(newLoopItem, after: nil) }
完整修复后的QueuePlayerManager代码
private class QueuePlayerManager: ObservableObject { let player: AVQueuePlayer private var isRemovedItems: Bool = false private let endAction: PlayerEndAction @Published private(set) var isReadyToPlay = false init( sequence: [String], endAction: PlayerEndAction ) { self.endAction = endAction var sequence = sequence if let lastItem = sequence.last { sequence.append(lastItem) } let urls: [URL] = sequence.map { Bundle.main.url( forResource: $0, withExtension: "mp4" )! } let playerItems = urls.map { AVPlayerItem(url: $0) } player = AVQueuePlayer(items: playerItems) player.actionAtItemEnd = .none if endAction != .none { NotificationCenter.default.addObserver( forName: .AVPlayerItemDidPlayToEndTime, object: player, queue: .main ) { [weak self] notification in guard let self = self else { return } let currentItem = notification.object as? AVPlayerItem if self.endAction == .loop, let currentItem = currentItem { // 创建新Item避免重复关联 guard let urlAsset = currentItem.asset as? AVURLAsset else { return } let newLoopItem = AVPlayerItem(url: urlAsset.url) if !self.isRemovedItems { // 首次播放结束后移除前置项目 let lastTwoItems = playerItems.suffix(2) playerItems.forEach { item in if !lastTwoItems.contains(item) { self.player.remove(item) } } self.isRemovedItems.toggle() } self.player.insert(newLoopItem, after: nil) self.player.advanceToNextItem() } } } NotificationCenter.default.addObserver( forName: .AVPlayerItemTimeJumped, object: player.currentItem, queue: .main ) { [weak self] _ in guard let self = self else { return } withAnimation { self.isReadyToPlay = true } } } deinit { NotificationCenter.default.removeObserver(self) player.removeAllItems() player.pause() } }
内容的提问来源于stack exchange,提问作者Daniil
相关产品推荐
相关产品推荐

