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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 10:27:09