8

I am using a sample code from Apple to play a video on a UICollectionViewCell background.

I'm using AVPlayerLooper, since it is a iteration of the same video.

My problem here is that when the video reaches the end, it shows a slight black-bip screen flash, maybe it is seeking the video to 0 time, I'm not sure.

Here's the code:

Protocol by Apple

protocol Looper {

    init(videoURL: URL, loopCount: Int)

    func start(in layer: CALayer)

    func stop()
}

Player Looper Class provided by Apple

// Code from Apple
class PlayerLooper: NSObject, Looper {
    // MARK: Types

    private struct ObserverContexts {
        static var isLooping = 0

        static var isLoopingKey = "isLooping"

        static var loopCount = 0

        static var loopCountKey = "loopCount"

        static var playerItemDurationKey = "duration"
    }

    // MARK: Properties

    private var player: AVQueuePlayer?

    private var playerLayer: AVPlayerLayer?

    private var playerLooper: AVPlayerLooper?

    private var isObserving = false

    private let numberOfTimesToPlay: Int

    private let videoURL: URL

    // MARK: Looper

    required init(videoURL: URL, loopCount: Int) {
        self.videoURL = videoURL
        self.numberOfTimesToPlay = loopCount

        super.init()
    }

    func start(in parentLayer: CALayer) {
        player = AVQueuePlayer()
        player?.isMuted = true
        playerLayer = AVPlayerLayer(player: player)

        guard let playerLayer = playerLayer else { fatalError("Error creating player layer") }
        playerLayer.frame = parentLayer.bounds
        parentLayer.addSublayer(playerLayer)

        let playerItem = AVPlayerItem(url: videoURL)
        playerItem.asset.loadValuesAsynchronously(forKeys: [ObserverContexts.playerItemDurationKey], completionHandler: {()->Void in
            /*
                The asset invokes its completion handler on an arbitrary queue when
                loading is complete. Because we want to access our AVPlayerLooper
                in our ensuing set-up, we must dispatch our handler to the main queue.
            */
            DispatchQueue.main.async(execute: {
                guard let player = self.player else { return }

                var durationError: NSError? = nil
                let durationStatus = playerItem.asset.statusOfValue(forKey: ObserverContexts.playerItemDurationKey, error: &durationError)
                guard durationStatus == .loaded else { fatalError("Failed to load duration property with error: \(String(describing: durationError))") }

                self.playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
                self.startObserving()
                player.play()
            })
        })
    }

    func stop() {
        player?.pause()
        stopObserving()

        playerLooper?.disableLooping()
        playerLooper = nil

        playerLayer?.removeFromSuperlayer()
        playerLayer = nil

        player = nil
    }

    // MARK: Convenience

    private func startObserving() {
        guard let playerLooper = playerLooper, !isObserving else { return }

        playerLooper.addObserver(self, forKeyPath: ObserverContexts.isLoopingKey, options: .new, context: &ObserverContexts.isLooping)
        playerLooper.addObserver(self, forKeyPath: ObserverContexts.loopCountKey, options: .new, context: &ObserverContexts.loopCount)

        isObserving = true
    }

    private func stopObserving() {
        guard let playerLooper = playerLooper, isObserving else { return }

        playerLooper.removeObserver(self, forKeyPath: ObserverContexts.isLoopingKey, context: &ObserverContexts.isLooping)
        playerLooper.removeObserver(self, forKeyPath: ObserverContexts.loopCountKey, context: &ObserverContexts.loopCount)

        isObserving = false
    }

    // MARK: KVO

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if context == &ObserverContexts.isLooping {
            if let loopingStatus = change?[.newKey] as? Bool, !loopingStatus {
                print("Looping ended due to an error")
            }
        }
        else if context == &ObserverContexts.loopCount {
            guard let playerLooper = playerLooper else { return }

            if numberOfTimesToPlay > 0 && playerLooper.loopCount >= numberOfTimesToPlay - 1 {
                print("Exceeded loop limit of \(numberOfTimesToPlay) and disabling looping");
                stopObserving()
                playerLooper.disableLooping()
            }
        }
        else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }
}

My collection view cell looper initialization

var looper: Looper? {
    didSet {
        configLooper()
    }
}
func configLooper() {
    looper?.start(in: layer)

}

My collection view delegate for cell initialization

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! FirstLaunchCollectionViewCell
    let videoURL = Bundle.main.url(forResource: "video3", withExtension: "MOV")
    cell.looper = PlayerLooper(videoURL: videoURL!, loopCount: -1)
    return cell
}

The loopCount set to -1 so the video playback infinite number of times.

I've tried using smaller video size files but it still shows a black frame at the end of each iteration.

Does anyone have any clue what might be causing this, or there's any better approach ? The Apple source code can be found here

Ivan Cantarino
  • 3,058
  • 4
  • 34
  • 73
  • Have you confirmed your videos dont start or end with a frame of black? – gadu Oct 31 '17 at 01:30
  • Yes I've confirmed - it doesn't start or end with black frame – Ivan Cantarino Oct 31 '17 at 01:31
  • Not the most helpful response, I'm afraid, but I remember dealing with this a few years ago (like iOS8) and couldn't figure out how to fix it, so ended up using a GitHub third party video player called SCPlayer. I wish you the best of luck! AVFoundation can sometimes be fun. You can maybe try adding an observer for when the video gets to JUST before its supposed to end and seek back to kCMTimeZero manually? – gadu Oct 31 '17 at 01:37
  • @gadu well yes I can try that ofc. In this case I'm curious why this is happening since it is a code sample provided by Apple. Can't figure out what am I doing wrong here :) – Ivan Cantarino Oct 31 '17 at 01:39
  • Yeah... I'd try what Zach Fuller suggests below and do it manually but when I tried using the AVQueuePlayer I had black gaps between my videos too. Seems they haven't fixed or it might be expected behavior – gadu Oct 31 '17 at 01:41

1 Answers1

0

Another way you could go about it is to add an observer for the video finishing like so:

NotificationCenter.default.addObserver(forName: AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: nil, using: { (_) in
   DispatchQueue.main.async {
      self.player.seek(to: kCMTimeZero)
      self.player.play()
   }
})
Zach Fuller
  • 1,219
  • 2
  • 14
  • 18
  • This method does not work for me. I get no error its just that it does not run –  Dec 17 '18 at 20:40