addPeriodicTimeObserver 保留 AVPlayer 实例

addPeriodicTimeObserver keeps AVPlayer instances

在遍历 Stack Overflow 和互联网最深处之后,我还没有找到解决方案。我希望外面的人能够帮助我解决困扰我好几天的问题。

有问题的应用程序有一个包含很多项目的集合视图。单击它时,您将进入预览集合视图。最后(我需要帮助的地方)当您单击 "GO" 时,您会看到一个集合视图,其中的单元格填满了整个屏幕。它由一个 AVPlayerLayerAVPlayer 组成。每次向右或向左滚动时,您都会看到另一个视频。 以下代码有效:

UICollectionViewCell

class PageCell: UICollectionViewCell {
    var player: AVPlayer?
    var playerLayer: AVPlayerLayer?
    var playerItem: AVPlayerItem?
    var videoAsset: AVAsset?
    var indexPath: IndexPath?

    var page: Page? {
         didSet {
             guard let unwrappedPage = page, let unwrappedIndexPath = indexPath else { return }
             addingVideo(videoID: unwrappedPage.steps[unwrappedIndexPath.item].video)
        }
    }

    func setupPlayerView() {
        player = AVPlayer()
        playerLayer = AVPlayerLayer(player: player)
        playerLayer?.videoGravity = .resizeAspectFill
        videoView.layer.addSublayer(playerLayer!)
        layoutIfNeeded()
        playerLayer?.frame = videoView.bounds
    }

    func addingVideo(videoID: String) {
        setupPlayerView()

        guard let url = URL(string: videoID) else { return }
        videoAsset = AVAsset(url: url)
        activityIndicatorView.startAnimating()
        videoAsset?.loadValuesAsynchronously(forKeys: ["duration"]) {
            guard self.videoAsset?.statusOfValue(forKey: "duration", error: nil) == .loaded else { return }
            self.player?.play()
            self.playerItem = AVPlayerItem(asset: self.videoAsset!)
            self.player?.replaceCurrentItem(with: self.playerItem)
    }
}

UICollectionViewController 中,我正在重复使用单元格。由于无法知道 AVPlayer 实例的数量,我只是在 PageCell 中创建了一个辅助变量,看起来三个单元格正在被重用(出列和重用单元格时的正常数量)。现在当我关闭这个 UICollectionViewControllerAVPlayer 实例似乎 disappear/close.

现在,当我想循环播放视频时,问题就出现了。使用 AVPlayerLooper 不是一种选择,因为它太慢了(我已经以不同的方式实现了它,但运气不佳)。所以我的解决方案是在 videoAsset?.loadValuesAsynchronously 块内使用周期时间观察器:

self.timeObserver = self.player?.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2),
    queue: DispatchQueue.global(), using: { (progressTime) in
    if let totalDuration = self.player?.currentItem?.duration{
        if progressTime == totalDuration {
            self.player?.seek(to: kCMTimeZero)
            self.player?.play()
        }
    }
})

问题

视频展示问题:Problem video

当同时有 +17 AVPlayer 个实例 运行 时会出现问题,然后视频突然不再加载。 iOS 设备的硬件限制是同时存在 4 到 18 个 AVPlayer 个实例 运行(很可能取决于 RAM),请参阅以下 Stack Overflow posts仅举几例:

How many AVPlayers are allowed to be created at the same time?

另请参阅这些文章以获得更多见解:

Building native video Pins

Too Many AVPlayers?

因为问题只发生在添加时间观察器时,我怀疑它保留了 AVPlayer 实例 "alive",即使我关闭了集合视图。

Problem video 的注释:每次我按下 "GO" 都会创建一个 AVPlayer 实例。当我向右滑动时,又创建了两个单元格,因此又创建了两个 AVPlayer 实例。每次总共创建 3 AVPlayer 个实例,最终累积到大约 17 个实例,视频将不再加载。每次按下 "GO" 时,我都尝试滚动浏览所有视频,但这不会改变结果,最多三个单元格一直被重复使用。

我尝试解决的问题

尝试 1: 我将 playerLayer 设为全局变量,并在 UICollectionViewController 内的 viewDidDisappear 中添加:

playerLayer?.player = nil

当我关闭 "GO" 页面上的单元格(即视图消失)时,这导致了一个 AVPlayer 实例到 disappear/close。这意味着我命中 17 个实例的时间比我没有添加此代码时晚了一点。除了上面的代码,我还尝试添加 playerLayer = nilplayerLayer?.player?.replaceCurrentItem(with: nil)playerLayer?.removeFromSuperlayer(),但唯一改变的是 playerLayer?.player = nil。应该注意的是,如果我不滚动并且只是打开第一个视频然后关闭,打开第一个视频然后关闭等等,我可以做到 "forever" 没有任何问题。因此,在这种情况下,创建了一个实例,然后是 closed/disappeared。

视频展示尝试 1:Try 1 video

尝试 2: 我将 addPeriodicTimeObserver 块更改为:

self.timeObserver = playerLayer?.player?.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 
    2), queue: DispatchQueue.global(), using: { (progressTime) in
    if let totalDuration = playerLayer?.player?.currentItem?.duration{
        if progressTime == totalDuration {
            playerLayer?.player?.seek(to: kCMTimeZero)
            playerLayer?.player?.play()
        }
    }
})

在此块中,我基本上将所有 self.player? 更改为 playerLayer?.player?

这使我能够随心所欲地打开和关闭视频视图 - 所以 AVPlayer 个实例不知何故 closing/disappearing。虽然现在循环不起作用。第一个视频最初会循环播放。但后来我滑动到第二个单元格,这个视频不会循环播放。然后我滑回第一个单元格,现在这也不会循环。如果我像在 "Try 1" 中那样添加 playerLayer?.player = nil,则打开和关闭以及循环都不会发生任何影响。

视频展示尝试 2:Try 2 video

尝试 3: 我将 timeObserver 变量设为全局变量并尝试了很多很多东西。但是当我试图删除观察者时,它总是导致以下错误:

'NSInvalidArgumentException', reason: 'An instance of AVPlayer cannot remove a time observer that was added by a different instance of AVPlayer.'

报错说明时间观察者明明到处都是。当我在 addPeriodicTimeObserver 块内添加辅助变量(计数器)并发现时间观察者迅速累加时,这一点就很清楚了。在某一时刻,使用此 post 中提供的代码的所有不同组合,我能够在滚动期间的 任何 时间删除时间观察器。但这导致仅从多个时间观察者中删除了一个时间观察者 - 导致与 "Try 1" 中相同的情况,每次我关闭视图时我都能够删除一个 AVPlayer 实例。

一般说明: 为了测试每次我按下 "GO" 并滑动时是否真的只有三个 AVPlayer 实例被创建,我做了一个测试,我滚动浏览了 20 多个视频,但加载它们没有问题。因此,我很确定,如前所述,每次在视图中最多创建三个 AVPlayer 个实例。

结论

问题是,正如我所见,当我添加时间观察器以循环播放视频时,它们会累积并保留 AVPlayer 个实例 "alive"。请记住,没有时间观察者,我完全没有问题。当应用 playerLayer?.player = nil 时,我似乎能够 "save" 一个实例,但是当我不知道当前 AVPlayer 个实例的数量时,又很难说 "active".简而言之,我想做的就是在视图消失的那一刻删除所有 AVPlayer 个实例,这将是快乐的日子。如果有人走到这一步,如果你能帮助我,我将不胜感激。

有一个保留周期。在转义闭包中使用 weak self。您创建的保留周期是:

cell -> Player -> observer closure -> cell

试试这个:

self.timeObserver = self.player?.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2),
    queue: DispatchQueue.global(), using: { [weak self] (progressTime) in
    if let totalDuration = self?.player?.currentItem?.duration{
        if progressTime == totalDuration {
            self?.player?.seek(to: kCMTimeZero)
            self?.player?.play()
        }
    }
})