Search code examples
iosswiftavplayeravplayeritemavplayerlayer

addPeriodicTimeObserver keeps AVPlayer instances


After being all over Stack Overflow and the deepest corners of the internet, I'm yet to find the solution. I hope someone out there will be able help me out with a problem I've had for days now.

The app in question has a collection view with lots of items. When you click on it you get to a preview collection view. And finally (where I need help) when you click "GO" you come to a collection view with cells that fills out the entire screen. It consists of an AVPlayerLayer and AVPlayer. Each time you scroll to right or left you see another video. The following code works:

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)
    }
}

In the UICollectionViewController I'm reusing cells. Since there is no way to know the number of AVPlayer instances, I simply made a helper variable inside the PageCell and it seems like three cells are getting reused (the normal amount when dequeuing and reusing cells). Now when I close this UICollectionViewController the AVPlayer instances seems to disappear/close.

Now, the problem arises when I want to loop the videos. Using AVPlayerLooper is not an option because it simply is too laggy (I've implemented it in a dusin different ways without luck). So my solution was to use a period time observer inside the videoAsset?.loadValuesAsynchronously block:

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 showcasing problem: Problem video

The problem arises when having +17 AVPlayer instances running simultaneously then suddenly the videos won't load anymore. iOS devices have a hardware limitation of everything from 4 to 18 AVPlayer instances running at the same time (most likely depending on RAM), see the following Stack Overflow posts just to mention a few:

AVPlayerItemStatusFailed error when too many AVPlayer instances created

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

AVPlayer not able to play after playing some items

Loading issues with AVPlayerItem from local URL

See also these articles for more insight:

Building native video Pins

Too Many AVPlayers?

Because the problem only occurs when adding the time observer I suspect that it keeps the AVPlayer instances "alive" even though I've closed the collection view.

Notes to Problem video: Every time I press "GO" one AVPlayer instance is created. When I swipe to the right two more cells are created hence two more AVPlayer instances. All in all 3 AVPlayer instances are created each time accumulating in the end to about 17 instances and the videos will not load anymore. I've tried scrolling through all the videos each time I've pressed "GO" but this does not change the outcome with a maximum of three cells being reused all the time.

What I've tried to solve the problem

Try 1: I made playerLayer a global variable and in viewDidDisappear inside the UICollectionViewController I added:

playerLayer?.player = nil

This resulted in one AVPlayer instance to disappear/close when I closed the cells onto the "GO" page (i.e. the view disappeared). This meant that I hit 17 instances a bit later than when I didn't add this code. Together with the code above I also tried adding playerLayer = nil, playerLayer?.player?.replaceCurrentItem(with: nil) and playerLayer?.removeFromSuperlayer() but the only thing that changed anything was playerLayer?.player = nil. It should be noted that if I do not scroll and simply open the first video and close, open the first video and close and so on, I could do it "forever" without any problems. So, in this case one instance was created and then closed/disappeared afterwards.

Video showcasing try 1: Try 1 video

Try 2: I changed the addPeriodicTimeObserver block to:

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()
        }
    }
})

In this block I essentially changed all self.player? to playerLayer?.player?.

This made me able to open and close the view with the videos as much as I wanted - so the AVPlayer instances were somehow closing/disappearing. Though now the looping didn't work. The first video would loop initially. But then I swiped to the second cell and this video would not loop. Then I swiped back to the first cell and now this wouldn't loop either. If I added playerLayer?.player = nil like in "Try 1" no effect occurred with neither the open and close nor the loops.

Video showcasing try 2: Try 2 video

Try 3: I made the timeObserver variable global and tried many many things. But when I tried to remove the observer(s) it always resulted in the following error:

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

The error indicates that the time observers are clearly all over the place. This was clear when I added helper variables (counters) inside the addPeriodicTimeObserver block and found out that the time observers were quickly adding up. At one point with all the different combinations of the code presented in this post, I was able to remove the time observer at any time during the scrolling. But this resulted in only removing a single time observer out of many - resulting in the same situation as in "Try 1" where I was able to remove a single AVPlayer instance each time I closed the view.

General note: To test whether or not it really was only three AVPlayer instances that were created each time I press "GO" and swiped I made a test where I scrolled through over 20 videos, but there was no problem loading them. Therefore I'm quite sure, as mentioned earlier, that a maximum of three AVPlayer instances are created in the view each time.

Conclusion

The problem is, as I see it, that when I add the time observers in order to loop the videos they accumulate and keep the AVPlayer instances "alive". Remember that I had no problem at all without the time observers. When applying playerLayer?.player = nil it seems as though, I was able to "save" an instance but then again it's so hard to tell when I don't know the amount of AVPlayer instances currently "active". Simply put, all I wanna do is to delete all AVPlayer instances the moment the view disappears and it will be happy days. If anyone got this far, I will be overly grateful if you are able to help me out.


Solution

  • There is a retain cycle. Use weak self in escaping closures. The retain cycle you created was:

    cell -> Player -> observer closure -> cell

    Try this:

    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()
            }
        }
    })