AVPlayer flashing when using replaceCurrentItem() - Swift - Programmatically

1.5k Views Asked by At

I'm setting up an AVPlayer in a UIView in this way:

fileprivate func setUpAVPlayer(){
    
    self.cardModel?.urlStrings?.forEach({ (urlString) in
        
        guard let url = URL(string: urlString) else { return }
        let asset = AVAsset(url: url)
        let playerItem = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: nil)
        playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: [.old, .new], context: nil)
        self.playerItemArray.append(playerItem)
    })
    player = AVPlayer(playerItem: playerItemArray[0])
}

In layoutSubviews I lay out the playerLayer that displays the player:

let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = .resizeAspectFill
self.playerView.layer.insertSublayer(playerLayer, at: 0)
playerLayer.frame = playerView.bounds

Upon a certain event I change the current item of the player:

fileprivate func goToNextVideo(){
    counter = counter+1
    counter = min(playerItemArray.count-1, counter)
    
    self.player?.replaceCurrentItem(with: self.playerItemArray[self.counter])
    self.player?.seek(to: .zero)
    self.player?.play()
}

When I change the item the transition is not smooth and I can see a flash displaying the view below. There are some things I don't understand:

  1. Why If I preloaded all the AVPlayerItems at the beginning (when I loop all urls strings) I still have to wait for them to be loaded when they become the currentItem of the AVPlayer?

  2. For what concern the flickering effect I searched online and didn't find a solution, so I meticulously inspected the behaviour of the AVPlayer whenever changing Item and I describe it in this way:

First It shows a white view (view below)

Second It shows a frame of the video it is going to display (more of less the timeframe is at half of video)

Third It actually displays the video

This behaviour is really annoying and I'm wondering how I can solve it.

1

There are 1 best solutions below

0
On
import UIKit
import GPUImage
import AVFoundation

class ViewController: UIViewController {

    lazy var movie: GPUImageMovie = {
        let movie = GPUImageMovie()
        movie.yuvConversionSetup()
        movie.addTarget(imageView)
        return movie
    }()
    
    lazy var imageView: GPUImageView = {
        let view = GPUImageView()
        view.setBackgroundColorRed(1, green: 1, blue: 1, alpha: 1)
        return view
    }()
    
    lazy var player: AVPlayer = {
        let player = AVPlayer()
        return player
    }()
    
    lazy var videoURLs: [URL] = {
        return [
            resource(filename: "video0.mp4"),
            resource(filename: "video1.mp4"),
            resource(filename: "video2.mp4"),
            resource(filename: "video3.mp4"),
        ].compactMap({ $0 })
    }()
    
    lazy var button: UIButton = {
        let button = UIButton()
        button.setTitle("replaceCurrentItem", for: .normal)
        button.setTitleColor(.gray, for: .normal)
        button.addTarget(self, action: #selector(action(_:)), for: .touchUpInside)
        return button
    }()
    
    var index = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(imageView)
        view.addSubview(button)
        imageView.frame = view.bounds
        button.frame = CGRect(x: 0, y: view.bounds.maxY - 100, width: view.bounds.width, height: 44)
    }

    func resource(filename: String) -> URL? {
        return Bundle.main.url(forResource: filename, withExtension: nil)
    }
    
    @objc func action(_ sender: UIButton) {
        if let url = getVideoURL() {
            player.pause()
            movie.endProcessing()
            movie.playerItem = AVPlayerItem(url: url)
            player.replaceCurrentItem(with: movie.playerItem)
            player.play()
            movie.startProcessing()
        }
    }
    
    func getVideoURL() -> URL? {
        if videoURLs.indices.contains(index) {
            let url = videoURLs[index]
            index = (index + 1) % videoURLs.count
            return url
        } else {
            return nil
        }
    }
    
}

The Podfile:

platform :ios, '9.0'
source 'https://github.com/CocoaPods/Specs.git'

target 'Test' do
  use_frameworks!
  pod 'GPUImage'
end

When you call replaceCurrentItem, it will clean the image buffer then wait to render the next frame. So, the key point is to keep the last frame on the screen: Instead of using AVPlayerLayer to render video frame, you can use AVPlayerItemVideoOutput to get the frame and render it using OpenGL ES (or Metal). You can also decide when to clean the image buffer.