12

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)
    }
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
  • Also how to achieve play pause when the player is in uitableviewcell or uicollectionviewcell – NickCoder Jun 03 '22 at 07:53
  • That’s a REALLY complicated thing to do, especially for it to work smoothly. In short the AVPlayer & AVPlayerItem has to be inside the cell but all of the AVPlayer observers have to be inside the vc. You need to use the delegation pattern to send the player & playerItem info back from the active cell to the vc. It’s a lot of work but that’s how you get it done. Use this as a starting point: https://stackoverflow.com/a/42029030. I used that answer to get me on the right path but I had to add a ton of code for it to work perfectly. It took me months to figure out. – Lance Samaria Jun 03 '22 at 08:10
  • I did what you said on scrollviewdidscroll and put mya observers in there and set object to player item notification didnt listen but when i remove it works but all players starts playing – NickCoder Jun 03 '22 at 09:32
  • What do you mean “I did what you said on scrollViewDidScroll”? I never said that. That answer isn’t my answer, reread what I wrote in the comment above, I said “I used that answer to get me on the right path but I had to add a ton of code for it to work perfectly”. Even if you read the comments underneath the actual answer (the linked answer), I left a comment that says “Had to make adjustments to the code ...”. It took literally months to get it working perfectly. I had to use answers and my own code from all over SO. It’s a really tough thing to do. – Lance Samaria Jun 03 '22 at 10:00
  • Btw I didn’t add the observers inside scrollViewDidScroll, that didn’t work for me. I don’t know your setup on when a video should play and when a previous video should stop playing. You need to 1. use whichever scrollView method works for your setup to determine at what point you want a video to play, 2. then use delegation pattern to trigger the observers, 3. then stop any other video that’s playing. Now that you’re on the right path, those 3 things are what will help you figure it out. It’s a lot of work, but that’s what worked for me. – Lance Samaria Jun 03 '22 at 10:07

5 Answers5

21

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

Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
5

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)
}
Tunscopi
  • 97
  • 2
  • 8
2

Add Observer

func addPlayerNotifications() {
    NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidPlayToEnd), name: .AVPlayerItemDidPlayToEndTime, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground), name: .UIApplicationWillEnterForeground, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground), name: .UIApplicationDidEnterBackground, object: nil)
}

Remove Observer

func removePlayerNotifations() {
    NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
    NotificationCenter.default.removeObserver(self, name: .UIApplicationWillEnterForeground, object: nil)
    NotificationCenter.default.removeObserver(self, name: .UIApplicationDidEnterBackground, object: nil)
}

Methods

// Player end.
@objc  func playerItemDidPlayToEnd(_ notification: Notification) {
    // Your Code.
    player.seek(to: kCMTimeZero)
}

//App enter in forground.
@objc func applicationWillEnterForeground(_ notification: Notification) {
      player.play()
}

//App enter in forground.
@objc func applicationDidEnterBackground(_ notification: Notification) {
      player.pause()
}

Try this code

vp2698
  • 1,783
  • 26
  • 28
  • what code should I enter in func playerItemDidPlayToEnd(_ notification: Notification) { // Your Code. } – Lance Samaria Jan 27 '18 at 07:36
  • `player.seek(to: kCMTimeZero)` this code. you posted in question – vp2698 Jan 27 '18 at 07:37
  • Did you try this? It doesn't work for me. Same problem occurs, the video freezes – Lance Samaria Jan 27 '18 at 07:41
  • What issue you are facing? – vp2698 Jan 27 '18 at 07:43
  • Same issue i said in the question. When the app goes to the background and I come back to it the video is frozen. According to Apple that's supposed to happen: https://developer.apple.com/library/content/qa/qa1668/_index.html#//apple_ref/doc/uid/DTS40010209-CH1-VIDEO – Lance Samaria Jan 27 '18 at 07:44
  • I tried the code and it is working. not freezing video. Are you using simulator or real device? – vp2698 Jan 27 '18 at 07:53
  • do this, put the app in the bg, open some other apps, the go back to it, close it, then immediately reopen it, its going to mess up. The way you suggested isn't the way to do otherwise Apple would not have said what it said in the link I posted. The way your doing it works if the user goes to the bg and comes back but if the user goes from foreground to background switching through apps multiple times then coming back to this one then it will freeze. It keeps happening to me – Lance Samaria Jan 27 '18 at 08:01
1

Tsonono answer works great, i just used it to fix a freezing video.

On a side not to get rid of the Drawback he's talking about ( video restarting every time you enter foreground ), just call the playeritself when using those 2 methods ( pause player in shutitdown method and play player in refresh method ):

@objc func refresh() {
self.player?.play()
@objc func shutItDown() {
self.player?.pause()

}

sonopower
  • 11
  • 2
  • This is exactly the solution. Just listen to the UIApplication Notifications and add these methods and it works. Tested on iOS 14 and 15. No need to set any variable to nil or to set it up again. – Fernando Cardenas Mar 11 '22 at 13:03
0

I found a simple solution that worked for me in Swift 4.3. I just created an observer for when the apps enter the background and when it enters the foreground in the overridden ViewDidLoad.

NotificationCenter.default.addObserver(self, selector:#selector(VideoViewController.shutItDown), name: UIApplication.didEnterBackgroundNotification, object: UIApplication.shared)
NotificationCenter.default.addObserver(self, selector:#selector(VideoViewController.refresh), name: UIApplication.willEnterForegroundNotification, object: nil)

Then I have the following methods called by the observers in the class:

@objc func refresh() {
    setupVideo()
}

@objc func shutItDown() {
    self.newLayer.removeFromSuperlayer()
}

where newLayer is my AVLayer that is added as a sublayer to my VideoView. Just to be more verbose, I have added the code for my video setup to make sure everything is understandable even though yours might look very different.

private func setupVideo() {

    self.path = URL(fileURLWithPath: Bundle.main.path(forResource: "coined", ofType: "mov")!)
    self.player = AVPlayer(url: self.path)

    self.newLayer = AVPlayerLayer(player: self.player)
    self.newLayer.frame = self.videoView.frame
    self.videoView.layer.addSublayer(newLayer)
    self.newLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill

    self.player.play()

    self.videoView.bringSubviewToFront(continueButton)
    self.videoView.bringSubviewToFront(settingsButton)
}

The "drawback" of this method is that the video is restarted each time you go from background to foreground. This is something that was acceptable in my case but might not be in yours. This is because the AVLayer is removed when you go to the background and I place a new AVLayer on the videoView each time you go to the foreground. The removal of the old AVLayer was fundamental in order to prevent a rendering snapshot error, in other words overcoming the "freeze".

TSonono
  • 45
  • 1
  • 4