ashvin10
ashvin10

Reputation: 73

Resuming AVPlayer after phone call

There appears to be many solutions on SO addressing this yet none of those solutions have worked for me. I'm currently using Swift 5. I have a AVPlayer playing an animation (that loops) in my ViewController. When a call comes in through CallKit, regardless of whether I answer or decline the call, the animation played by the AVPlayer does not resume after the call has been dealt with. The interruption handler seems to be called before an interruption but usually doesn't get called after the interruption.

        override func viewDidLoad() {
            super.viewDidLoad()
            prepareBGVideo()
            ...
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(applicationWillEnterForeground(notification:)),
                name: UIApplication.willEnterForegroundNotification,
                object: nil)
            ...
        }        

       func prepareBGVideo() {
            guard let path = Bundle.main.path(forResource: "animation", ofType:"mp4") else {
                print("video not found")
                return
            }

            let item = AVPlayerItem(url: URL(fileURLWithPath: path))
            avPlayer = AVPlayer(playerItem: item)

            NotificationCenter.default.addObserver(self,
                                                   selector: #selector(loopVideoBG),
                                                   name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
                                                   object: item)
            NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption(notification:)), name: AVAudioSession.interruptionNotification, object: nil)
            avPlayerLayer = AVPlayerLayer(player: avPlayer)
            avPlayerLayer.backgroundColor = UIColor.black.cgColor
            avPlayer.volume = 0
            avPlayer.actionAtItemEnd = .none
            avPlayer.play()

            view.backgroundColor = .clear
            avPlayerLayer.frame = view.layer.bounds
            view.layer.insertSublayer(avPlayerLayer, at: 0)
            avPlayerLayer.videoGravity = isIPAD ? AVLayerVideoGravity.resize : AVLayerVideoGravity.resizeAspectFill // Changed from AVLayerVideoGravity.resizeAspect to AVLayerVideoGravity.resize so that video fits iPad screen

            NotificationCenter.default.addObserver(self,
                                                   selector: #selector(willEnterForeground),
                                                   name: UIApplication.willEnterForegroundNotification,
                                                   object: nil)
        }

        @objc func handleInterruption(notification: Notification) {
            guard let info = notification.userInfo,
                let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
                let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
                    return
            }
            if type == .began {
                // Interruption began, take appropriate actions (save state, update user interface)
                self.avPlayer.pause()
            } else if type == .ended {
                guard let optionsValue =
                    info[AVAudioSessionInterruptionOptionKey] as? UInt else {
                        return
                }
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) {
                    // Interruption Ended - playback should resume
                    self.avPlayer.play()
                }
            }
        }

        /// Resume video while app wake up from background
        @objc func willEnterForeground() {
            avPlayer.seek(to: CMTime.zero)
            JPUtility.shared.performOperation(0.1) {
                self.avPlayer.play()
            }
        }

        @objc func loopVideoBG() {
            avPlayer.seek(to: CMTime.zero)
            avPlayer.play()
        }

Here are all the solutions that I have tried:

  1. Waiting two seconds before calling self.avPlayer.play() in if options.contains(.shouldResume){}
  2. Setting AVAudioSession.sharedInstance().setActive to false when interruption begins and then setting it ot true when interruption ends. The issue with this approach is that the if interruption == .ended {} block doesn't always get invoked so setting setActive had no effect.
  3. Setting AVAudioSession playback category to AVAudioSessionCategoryOptions.MixWithOthers. My animation doesn't have audio anyway.

I have seen mentions of resuming playback in applicationDidBecomeActive(_:) but some advised against this. Would this be considered good practice?

Is there a way to ensure that the else if type == .ended {} block gets executed? Or perhaps a workaround that works more reliably than observing AVAudioSession.interruptionNotification?

Upvotes: 2

Views: 1479

Answers (1)

ashvin10
ashvin10

Reputation: 73

I solved this but creating a shared VideoPlayer class that contained references to all the screen that had animations.

import Foundation
import UIKit
import AVKit

    class VideoPlayer: NSObject {

        static var shared: VideoPlayer = VideoPlayer()

        var avPlayer: AVPlayer!
        var avPlayerLayer: AVPlayerLayer!

        weak var vcForConnect:ConnectVC?
        weak var vcForList:ListVC?

        override init() {
            super.init()
            guard let path = Bundle.main.path(forResource: "animation", ofType:"mp4") else {
                print("video not found")
                return
            }
            avPlayer = AVPlayer(url: URL(fileURLWithPath: path))
            avPlayerLayer = AVPlayerLayer(player: avPlayer)
            avPlayerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
            avPlayer.volume = 0
            avPlayer.actionAtItemEnd = .none
            loopVideo(videoPlayer: avPlayer)
            avPlayer.play()
            NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption(notification:)), name: AVAudioSession.interruptionNotification, object: nil)

        }

        deinit {
            avPlayer.pause()
        }

        @objc func handleInterruption(notification: Notification) {
            guard let info = notification.userInfo,
                let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
                let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
                    return
            }
            if type == .began {
                // Interruption began, take appropriate actions (save state, update user interface)
                self.avPlayer.pause()
            } else if type == .ended {
                guard let optionsValue =
                    info[AVAudioSessionInterruptionOptionKey] as? UInt else {
                        return
                }
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) {
                    // Interruption Ended - playback should resume
                    self.avPlayer.play()
                }
            }
        }

        func resumeAllAnimations() {
            self.avPlayer.play()
            if vcForList?.avPlayer != nil {
                vcForList?.avPlayer.play()
            }
            if vcForConnect?.avPlayer != nil {
                vcForConnect?.avPlayer.play()
            }
            if vcForConnect?.avPlayerBG != nil {
                vcForConnect?.avPlayerBG.play()
            }
        }
        ...
    }

I then resume the animations by calling resumeAllAnimations() in applicationDidBecomeActive(_:) in AppDelegate.swift like so:

func applicationDidBecomeActive(_ application: UIApplication) {
        VideoPlayer.shared.resumeAllAnimations()
        ...
}

Upvotes: 1

Related Questions