3

I'm using AVKit to show videos to a user.

When the user selects a video it is presented using the standard player from AVKit. This makes it fullscreen.

If the user elects to make it 'Picture in Picture' the user is able to continue using the rest of the application.

This means the user is able to choose another video to play, which indeed does play (in fullscreen) at the same time as the previous video which is still visible in picture in picture.

I'd like to close the first (PiP) video when the second video is selected, however, before launching the 2nd video I try to 'dismiss' the previous, but it doesn't work.

I think it's because it's being shown as a PiP so the AVPlayerViewController doesn't represent it anymore...

Is there a way to do this simply?

Playing a video with the (not working) attempt to kill the first video if the second is trying to be played:

func play(FileName filename: String, FileType type: String)
{
    if self.isVideoPlaying == YES
    {
       self.playerController!.dismiss(animated: YES, completion: { self.isVideoPlaying = NO ; self.play(FileName: filename, FileType: type) })

       return
    }

    self.isVideoPlaying = YES

    let path = Bundle.main.path(forResource: filename, ofType: type)

    let url = NSURL(fileURLWithPath: path!)

    let player = AVPlayer(url: url as URL)

...
iOSProgrammingIsFun
  • 1,418
  • 1
  • 15
  • 32

2 Answers2

3

Ok, here's how I solved it:

  1. Have a class property of 'isAlreadyPlaying: Bool' that you set when you play a video... however

  2. In the method where you 'play' the video and present the AVPlayerViewController with the (new) video, have this check at the beginning:


if self.isAlreadyPlaying == YES
{
     killVideoPlayer()

     DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: { self.play(FileName: filename, FileType: type) })

     return
}

... Here you put the normal play / present code

And in the 'killVideoPlayer()' method, have the property 'isAlreadyPlaying' set to NO / false.

i.e.

(Just for those who don't already know), here's the kill function:

private func killVideoPlayer()
{
    self.isAlreadyPlaying          = NO

    self.playerController?.player?.pause()
    self.playerController?.player  = nil

    let audioSession = AVAudioSession.sharedInstance()

    do
    {
        try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
        try audioSession.setCategory(.soloAmbient)
    }
    catch
    {
        print("Audio session failed")
    }

    self.playerController?.dismiss(animated: YES, completion: 
    { 
        self.playerController = nil 
    })
}
iOSProgrammingIsFun
  • 1,418
  • 1
  • 15
  • 32
  • I'm having the same issue, and your solution isn't clear to me. How do you actually kill the existing picture in picture? – R Moyer Jan 16 '20 at 15:57
  • 2
    @RMoyer You just do it the standard way that Apple demonstrates in their code / tutorials. Pause the player and nil all references and dismiss the AVPVC. – iOSProgrammingIsFun Jan 17 '20 at 16:28
  • 1
    @RMoyer I've amended the answer to include an example Kill method. – iOSProgrammingIsFun Jan 17 '20 at 16:35
  • @iOSProgrammingIsFun Have you figure out another solution without the use of a timer? – Alejandro Cotilla Jan 11 '21 at 15:10
  • @AlejandroCotilla Yes. My answer above. – iOSProgrammingIsFun Jan 12 '21 at 16:04
  • @iOSProgrammingIsFun I am facing a similar problem in tvOS, killing the player will stop PIP, but the PIP window will remain in the screen for a while. If we close the PIP using the close button inside PIP, it will close immediately. But if I kill the playback PIP window will stay on the screen for 2 to 4 seconds with a black background. Is there any other solution to this problem? – Maneesh M Mar 01 '22 at 13:19
  • Hi @ManeeshM Have you found any solution for the same in tvOS?? – priyanshu Jul 06 '23 at 07:58
  • No @priyanshu. It seems like apple doesn't like us to close the PIP programatically. – Maneesh M Jul 07 '23 at 07:45
0

SwiftUI and UIViewControllerRepresentable

My issue was that I wanted to close the last video played Picture in Picture when a new video was played. I solved it using Combine and the functions of the AVPlayerViewControllerDelegate.

The coordinator keeps track of the PIP status of the currently played video and stores that AVPlayerViewController in a manager Singleton in case that video is in PIP. Once a new video is played that already stored AVPlayerViewController is dismissed.

struct PlayerView: UIViewControllerRepresentable {

private let playerViewController: AVPlayerViewController = {
    AVPlayerViewController()
}()

public func makeCoordinator() -> Coordinator {
    Coordinator()
}

func makeUIViewController(context: Context) -> AVPlayerViewController {
    playerViewController.delegate = context.coordinator
    context.coordinator.playerController = playerViewController
    return playerViewController
}

func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) { }

class Coordinator: NSObject, AVPlayerViewControllerDelegate {
    weak var playerController: AVPlayerViewController? {
        didSet {
            playerController?.delegate = self
        }
    }
    
    private var cancellables: Set<AnyCancellable> = Set<AnyCancellable>()
    
    private var videoisPlayedInPip = PassthroughSubject<Bool, Never>()
    private var pictureInPictureManager: VideoPlayerPictureInPictureManager = .shared
    
    init() {
        super.init()

        handlePictureInPictureChanges()
    }
    
    private func handlePictureInPictureChanges() {
        //Dismiss video played in PIP when a new video is played 
        self.pictureInPictureManager.closeLastPipPlayer()

        // Update new video played in PIP 
        videoisPlayedInPip.sink { [weak self] isInPip in
            guard let self else { return }
            if isInPip {
                self.pictureInPictureManager.updateLastPiPAVPlayerVC(self.playerController)
            } else {
                self.pictureInPictureManager.updateLastPiPAVPlayerVC(nil)
            }
       }
        .store(in: &cancellables)
    }
    
   func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
        videoisPlayedInPip.send(true)
    }
    
    func playerViewControllerWillStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
        videoisPlayedInPip.send(false)
    }
} 
}

class VideoPlayerPictureInPictureManager {

public static let shared = VideoPlayerPictureInPictureManager()

private(set) var lastPiPAVPlayerVC: AVPlayerViewController?

//Dismiss video played in PIP
public func closeLastPipPlayer() {
    lastPiPAVPlayerVC?.dismiss(animated: false)
    lastPiPAVPlayerVC?.player?.pause()
    lastPiPAVPlayerVC?.player = nil
    
    DispatchQueue.main.async { [weak self] in
        self?.lastPiPAVPlayerVC?.dismiss(animated: true, completion: {
            self?.updateLastPiPAVPlayerVC(nil)
        })
    }
}

//Update new video played in PIP 
public func updateLastPiPAVPlayerVC(_ vc: AVPlayerViewController?) {
    self.lastPiPAVPlayerVC = vc
}
}