Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift iOS -AVPlayer Video Freezes / Pauses When App Comes Back from Background

I have a video playing in a loop on the login page of my app. I followed this Youtube tutorial to get it to work loop video in view controller

The problem is when the app goes to the background, if I don't come back right away, when i do come back the video gets frozen.

According to the Apple Docs that's supposed to happen.

I tried to use the NotificationCenter's Notification.Name.UIApplicationWillResignActive but that didn't work.

How do I get the video to keep playing once the app returns from the background?

var player: AVPlayer!
var playerLayer: AVPlayerLayer!

override func viewDidLoad() {
        super.viewDidLoad()

        configurePlayer()
}


@objc fileprivate func configurePlayer(){

        let url = Bundle.main.url(forResource: "myVideo", withExtension: ".mov")

        player = AVPlayer.init(url: url!)
        playerLayer = AVPlayerLayer(player: player!)
        playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
        playerLayer.frame = view.layer.frame


        player.actionAtItemEnd = AVPlayerActionAtItemEnd.none

        player.play()

        view.layer.insertSublayer(playerLayer, at: 0)

        NotificationCenter.default.addObserver(self, selector: #selector(playerItemReachedEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)

        NotificationCenter.default.addObserver(self, selector: #selector(playerItemReachedEnd), name: Notification.Name.UIApplicationWillResignActive, object: player.currentItem)

    }

@objc fileprivate func playerItemReachedEnd(){
        player.seek(to: kCMTimeZero)
    }
like image 884
Lance Samaria Avatar asked Jan 27 '18 06:01

Lance Samaria


2 Answers

According to the Apple Docs when a video is playing and the app is sent to the background the player is automatically paused:

enter image description here

What they say to do is remove the AVPlayerLayer (set to nil) when the app is going to the background and then reinitialize it when it comes to the foreground:

enter image description here

And the best way they say to handle this is in the applicationDidEnterBackground and the applicationDidBecomeActive:

enter image description here

I used NSNotification to listen for the background and foreground events and set functions to pause the player & set the playerLayer to nil (both for background event) and then reinitialized the playerLayer & played the player for the foreground event. These are the Notifications I used .UIApplicationWillEnterForeground and .UIApplicationDidEnterBackground

What I've come to find out is that for some reason if you long press the Home button and that screen that pops up that says "What can I help you with" appears, if you press the Home button again to go back to your app the video will be frozen and using the 2 Notifications from above won't prevent it. The only way I found to prevent this is to also use the Notifications .UIApplicationWillResignActive and .UIApplicationDidBecomeActive. If you don't add these in addition to the above Notifications then your video will be frozen on the Home button long press and back. The best way that I've found to prevent all frozen scenarios is to use all 4 Notifications.

2 things I had to do differently from my code above was to set player and playerLayer class variables as optionals instead of implicitly unwrapped optionals and I also added an extension to the AVPlayer class to check to see if it's playing or not in iOS 9 or below. In iOS 10 and above there is a built in method .timeControlStatus AVPlayer timer status

my code above:

var player: AVPlayer?
var playerLayer: AVPlayerLayer?

Add an extension to the AVPlayer to check the state of the AVPlayer in iOS 9 or below:

import AVFoundation

extension AVPlayer{

    var isPlaying: Bool{
        return rate != 0 && error == nil
    }
}

Here is the completed code below:

var player: AVPlayer?
var playerLayer: AVPlayerLayer? //must be optional because it will get set to nil on background event

override func viewDidLoad() {
    super.viewDidLoad()

    // background event
    NotificationCenter.default.addObserver(self, selector: #selector(setPlayerLayerToNil), name: UIApplication.didEnterBackgroundNotification, object: nil)

    // foreground event
    NotificationCenter.default.addObserver(self, selector: #selector(reinitializePlayerLayer), name: UIApplication.willEnterForegroundNotification, object: nil)

   // add these 2 notifications to prevent freeze on long Home button press and back
    NotificationCenter.default.addObserver(self, selector: #selector(setPlayerLayerToNil), name: UIApplication.willResignActiveNotification, object: nil)

    NotificationCenter.default.addObserver(self, selector: #selector(reinitializePlayerLayer), name: UIApplication.didBecomeActiveNotification, object: nil)

    configurePlayer()
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    // this is also for the long Home button press
    if let player = player{
        if #available(iOS 10.0, *) {
            if player.timeControlStatus == .paused{
                player.play()
            }
        } else {
            if player.isPlaying == false{
                player.play()
            }
        }
    }
}

@objc fileprivate func configurePlayer(){

    let url = Bundle.main.url(forResource: "myVideo", withExtension: ".mov")

    player = AVPlayer.init(url: url!)
    playerLayer = AVPlayerLayer(player: player!)
    playerLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
    playerLayer?.frame = view.layer.frame

    player?.actionAtItemEnd = AVPlayerActionAtItemEnd.none

    player?.play()

    view.layer.insertSublayer(playerLayer!, at: 0)

    NotificationCenter.default.addObserver(self, selector: #selector(playerItemReachedEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
}

@objc fileprivate func playerItemReachedEnd(){
     // this works like a rewind button. It starts the player over from the beginning
     player?.seek(to: kCMTimeZero)
}

 // background event
@objc fileprivate func setPlayerLayerToNil(){
    // first pause the player before setting the playerLayer to nil. The pause works similar to a stop button
    player?.pause()
    playerLayer = nil
}

 // foreground event
@objc fileprivate func reinitializePlayerLayer(){

    if let player = player{

        playerLayer = AVPlayerLayer(player: player)

        if #available(iOS 10.0, *) {
            if player.timeControlStatus == .paused{
                player.play()
            }
        } else {
            // if app is running on iOS 9 or lower
            if player.isPlaying == false{
                player.play()
            }
        }
    }
}

DON'T FORGET TO ADD THE isPlaying EXTENSION TO THE AVPlayer

like image 144
Lance Samaria Avatar answered Nov 17 '22 11:11

Lance Samaria


The accepted answer did not work for me. My "welcome" video randomly paused on certain occasions.


Here's what did:
Background: Since the player and playerLayer objects do not get destroyed when the app "resignsActive" or goes into the "background" (which can be verified by observing their states when their respective notifications are called) I surmised setting either of these objects to nil and then re-initializing them on entering background or foreground is a little unnecessary.

I only play the player object again when it will enter the foreground.

var player: AVPlayer?
var playerLayer: AVPlayerLayer?

In ViewDidLoad, I configure my player object.

override func viewDidLoad() {
  configurePlayer()
}

The configurePlayer() function is defined below

private func configurePlayer() {
  guard let URL = Bundle.main.url(forResource: "welcome", withExtension: ".mp4") else { return }

  player = AVPlayer.init(url: URL)
  playerLayer = AVPlayerLayer(player: player)
  playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
  playerLayer?.frame = view.layer.frame

  player?.actionAtItemEnd = AVPlayerActionAtItemEnd.none
  playItem()

  setupPlayNotificationItems()
}

And here are the helper functions implementations

private func setupPlayNotificationItems() {
  NotificationCenter.default.addObserver(self,
                                        selector: #selector(restartPlayerItem),
                                        name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
                                        object: player?.currentItem)
  NotificationCenter.default.addObserver(self,
                                        selector: #selector(playItem),
                                        name: .UIApplicationWillEnterForeground,
                                        object: nil)
}

@objc private func playItem() {
  // If you please, you can also restart the video here
  restartPlayerItem()

  player?.play()

  if let playerlayer = playerLayer {
    view.layer.insertSublayer(playerlayer, at: 0)
  }
}

@objc func restartPlayerItem() {
  player?.seek(to: kCMTimeZero)
}
like image 5
Tunscopi Avatar answered Nov 17 '22 12:11

Tunscopi