SwiftUI: Allowing user to enable Picture-in-Picture with the press of a button (iOS)

2.9k Views Asked by At

I'm designing an app that allows the user to view a certain video in a SwiftUI view. I want to support PiP, and AVPictureInPictureController.isPictureInPictureSupported() always returns true, while pipController.isPictureInPicturePossible sometimes returns true, but not usually while the video is actually playing. It's possible I misunderstand how to use AVPictureInPictureController.

My app initializes a view, called a FullScreenVideoView, with the following code:

init(player: KHKVideoPlayer, aspectRatio: CGFloat, presentingViewController pvc: UIViewController?) {
    self.aspectRatio = aspectRatio
    self.pvc = pvc
    self.model = FullScreenVideoViewerModel(player: player)
    self.playerController = KHKPlayerController(player: player.avPlayer, cornerRadius: 0)
    
    if AVPictureInPictureController.isPictureInPictureSupported() {
        self.pipController = AVPictureInPictureController(playerLayer: AVPlayerLayer(player: player.avPlayer))!
    }
    
    self.isMuted = player.avPlayer.isMuted
}

Then, inside the view body, it displays the video by calling the playerController defined in the initializer:

playerController
                    .frame(height: aspectRatio * geometry.size.width)
                    .onTapGesture {
                        if self.model.player.avPlayer.isPlaying {
                            self.model.pause()
                        } else {
                            self.model.play()
                        }
                    }

The playerController is a wrapper on an AVPlayerViewController:

struct KHKPlayerController: UIViewControllerRepresentable {
typealias UIViewControllerType = AVPlayerViewController

var player: AVPlayer
var cornerRadius: CGFloat?

init(player: AVPlayer) {
    self.player = player
}

init(player: AVPlayer, cornerRadius: CGFloat) {
    self.player = player
    self.cornerRadius = cornerRadius
}

func makeUIViewController(context: Context) -> AVPlayerViewController {
    let playerVC = AVPlayerViewController()
    playerVC.showsPlaybackControls = false
    playerVC.player = player
    playerVC.view.layer.masksToBounds = true
    playerVC.view.layer.cornerRadius = cornerRadius ?? 8
    return playerVC
}

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

}

A button is supposed to start PiP:

RectangleButton(action: {
                                print("pip is possible: \(pipController?.isPictureInPicturePossible ?? false)")
                                pipController?.startPictureInPicture()
                            }, imageSystemName: "pip.enter", foregroundColor: UI.Constants.primaryColor, backgroundColor: UI.Constants.tertiaryColor, width: 35, height: 25, cornerRadius: 3)

Any help would be much appreciated. I can't find any helpful information on starting PiP in SwiftUI.

Note- the correct background capability is enabled. Also, in my AppDelegate didFinishLaunchingWithOptions method, I have this code that seems to be required for PiP:

do {
        try audioSession.setCategory(AVAudioSession.Category.playback)
    } catch  {
        print("Audio session failed")
    }
1

There are 1 best solutions below

1
On

Your code actually mostly works - at least inside my SwiftUI-code I pretty much have the same steps as you described.

However, I do a bit more on the playerLayer and maybe this causes the pipController to actually work.

Here is what I do with the playerLayer inside my View-definition, I add it as a separate controls layer:

player = AVPlayer(url: url)
playerLayer = AVPlayerLayer()

playerLayer.player = player
layer.addSublayer(playerLayer)
        
playerLayer.videoGravity = .resizeAspect
playerLayer.frame = self.bounds

if let playerLayer = playerLayer {
    if AVPictureInPictureController.isPictureInPictureSupported() {
        pipController = AVPictureInPictureController(playerLayer: playerLayer)!
    }
}

I am not 100% sure if this extra sublayer is really causing the pipController to improve.

Also, I am not using KHKVideoPlayer but rather plain AVPlayer(url: url).

And I am not using AVPlayerViewController at all since doing all in SwiftUI.

And of course, I also add the Background-mode to make pip possible...

enter image description here