skip to Main Content

I have AVPlayer with AVPictureInPictureController. Play video in app and picture In Picture works except one situation.
Issue is: I pause video in application and during switch to background is not PiP activate. What do I wrong?

import UIKit
import AVKit
import AVFoundation

class ViewControllerSec: UIViewController,AVPictureInPictureControllerDelegate {
    var pipPlayer: AVPlayer!
    var avCanvas : UIView!
    var pipCanvas: AVPlayerLayer?
    var pipController: AVPictureInPictureController!
    var mainViewControler : UIViewController!
    var playerItem : AVPlayerItem!
    var videoAvasset : AVAsset!

    public func link(to parentViewController : UIViewController) {
        mainViewControler = parentViewController
        setup()
    }

    @objc func appWillResignActiveNotification(application: UIApplication) {
        pipController.startPictureInPicture()
    }

    private func setupAudio() {
        do {
            let session = AVAudioSession.sharedInstance()
            try session.setCategory(.playback, mode: .moviePlayback)
            try session.setActive(true)
        } catch {
            print("Audio session setup failed: (error.localizedDescription)")
        }
    }

    
    @objc func playerItemDidFailToPlayToEnd(_ notification: Notification) {
        if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error {
            print("Failed to play to end: (error.localizedDescription)")
        }
    }
    
    func setup() {
        setupAudio()
        // 1. Set up AVPlayer
        guard let videoURL = URL(string: "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.m3u8") else { return }
        videoAvasset = AVAsset(url: videoURL)
        playerItem = AVPlayerItem(asset: videoAvasset)

        addPlayerObservers()
        pipPlayer = AVPlayer(playerItem: playerItem)

        // 2. Set up AVPlayerLayer
        avCanvas = UIView(frame: view.bounds)
        pipCanvas = AVPlayerLayer(player: pipPlayer)
        guard let pipCanvas else { return }
        pipCanvas.frame = avCanvas.bounds
        //pipCanvas.videoGravity = .resizeAspectFill
               
        mainViewControler.view.addSubview(avCanvas)
        avCanvas.layer.addSublayer(pipCanvas)
        
        
        // 3. Set up AVPictureInPictureController
        if AVPictureInPictureController.isPictureInPictureSupported() {
            pipController = AVPictureInPictureController(playerLayer: pipCanvas)
            pipController?.delegate = self
            pipController?.canStartPictureInPictureAutomaticallyFromInline = true
        }
        
        let playButton = UIButton(frame: CGRect(x: 20, y: 50, width: 100, height: 50))
        playButton.setTitle("Play", for: .normal)
        playButton.backgroundColor = .blue
        playButton.addTarget(self, action: #selector(playTapped), for: .touchUpInside)
        mainViewControler.view.addSubview(playButton)

        let pauseButton = UIButton(frame: CGRect(x: 140, y: 50, width: 100, height: 50))
        pauseButton.setTitle("Pause", for: .normal)
        pauseButton.backgroundColor = .red
        pauseButton.addTarget(self, action: #selector(pauseTapped), for: .touchUpInside)
        mainViewControler.view.addSubview(pauseButton)

        let pipButton = UIButton(frame: CGRect(x: 260, y: 50, width: 150, height: 50))
        pipButton.setTitle("Start PiP", for: .normal)
        pipButton.backgroundColor = .green
        pipButton.addTarget(self, action: #selector(startPictureInPicture), for: .touchUpInside)
        mainViewControler.view.addSubview(pipButton)
        print("Error:(String(describing: pipPlayer.error?.localizedDescription))")
        
        // Observe application state changes
        NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
            guard let self = self else { return }
            if self.pipPlayer.rate == 0 {
                self.pipPlayer.play()
                pipController?.startPictureInPicture()
            }
        }

    func addPlayerObservers() {
         // Observe player item's status
         playerItem?.addObserver(self, forKeyPath: "status", options: [.old, .new], context: nil)
         
         // Observe playback completion
         NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: playerItem)
     }

     // Observe player status changes
     override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
         if keyPath == "status" {
             if let statusNumber = change?[.newKey] as? NSNumber {
                 let status = AVPlayer.Status(rawValue: statusNumber.intValue)!
                 switch status {
                 case .readyToPlay:
                     print("Player is ready to play")
                 case .failed:
                     print("Player failed: (String(describing: playerItem?.error))")
                 case .unknown:
                     print("Player status is unknown")
                 @unknown default:
                     fatalError()
                 }
             }
         }
     }
     
     @objc func playerDidFinishPlaying(_ notification: Notification) {
         print("Video finished playing.")
     }
     
     deinit {
         // Clean up observers
         playerItem?.removeObserver(self, forKeyPath: "status")
         NotificationCenter.default.removeObserver(self)
     }
    
    @objc func playTapped() {
        pipPlayer.play()
    }

    @objc func pauseTapped() {
        pipPlayer.pause()
    }
    
    @objc func startPictureInPicture() {
        if let pipController = pipController, !pipController.isPictureInPictureActive {
            pipController.startPictureInPicture()
        }
    }

    @objc func stopPictureInPicture() {
        if let pipController = pipController, pipController.isPictureInPictureActive {
            pipController.stopPictureInPicture()
        }
    }

    // MARK: - AVPictureInPictureControllerDelegate Methods

    func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("PiP will start")
    }

    func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("PiP did start")
    }

    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
        print("Failed to start PiP: (error.localizedDescription)")
        if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] {
             print("Underlying error: (underlyingError)")
         }
    }

    func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("PiP will stop")
    }

    func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("PiP did stop")
    }
}

I try use observers with appWillResignActiveNotification to activate PiP. It is call before observer appDidEnterBackground but this also do not solve my problem, also tryied to start play video in observers and then activate PiP with the same result.
I am getting error in event: Failed to start PiP: Failed to start picture in picture.

2

Answers


  1. Chosen as BEST ANSWER

    I found solution. Problem was in UIScene when appliaction did to background. AVPictureInPicture need during activation also active UIScene.

    I changed Observer to uiSceneWillDeactivate where I activate PiP

    NotificationCenter.default.addObserver(
        self,
        selector: #selector(uiSceneWillDeactivate),
        name: UIScene.willDeactivateNotification,
        object: nil
    )
    

  2. Refined Implementation

    1. Ensure Player Readiness Before Starting PiP
      Add a check for AVPlayerItem.status in appWillResignActiveNotification(_ notification:_):

        guard let pipController = pipController,pipController.isPictureInPictureSupported else {
           print("PiP not supported")
           return
       }
      
       // Ensure player is ready to play
       if playerItem.status == .readyToPlay {
           if pipPlayer.rate == 0 { // If paused
               pipPlayer.play() // Briefly play to ensure PiP starts
           }
           pipController.startPictureInPicture()
       } else {
           print("Player not ready for PiP.")
       }
      
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search