iOS Picture in Picture player controls visibility

In SDK version 3.16.0 it was possible to control the visibility of the player controls in the PiP window. For example when the users are watching a livestream, we disabled the seekstack buttons (+/- 10 seconds) and the play/pause button. This was done via the following code inside a subclass of the PlayerView:

let controller = AVPictureInPictureController(playerLayer: self.layer as! AVPlayerLayer)

// disable seekstack buttons, display [Live] tag inside player view
controller?.requiresLinearPlayback = true 

// determine play/pause visibility
controller?.setValue(1, forKey: "controlsStyle") 

When updating the SDK to the latest version, 3.29.0 as of writing, the above implementations seize to work. Can we get an opening in the Bitmovin SDK for these? Legally we’re required to disable any seek actions on some of our livestreams.

Hi Mart,

Thanks for posting this. Could you please give the below code a try on your end? It’s a modified version of our BasicPictureInPicture app after following this article, and it seems to work fine for me both on 3.16.0 and 3.29.0 on iOS16.1. Note that I’m calling self.playerView.enterPiP() instead of self.playerView.enterPictureInPicture()

import UIKit
import Foundation //added by alberto
import BitmovinPlayer


//added by alberto
class CustomPiPView: PlayerView, AVPictureInPictureControllerDelegate {
    var controller: AVPictureInPictureController?
    override init(player: Player, frame: CGRect) {
        super.init(player: player, frame: frame)
        let layer = self.layer as! AVPlayerLayer
        if AVPictureInPictureController.isPictureInPictureSupported() {
            self.controller = AVPictureInPictureController(playerLayer: layer)
            self.controller?.delegate = self
            //added by alberto
            if #available(iOS 14.0, *) {
                self.controller?.requiresLinearPlayback = true
                self.controller?.setValue(1, forKey: "controlsStyle")
            } else {
                // Fallback on earlier versions
            }
        }
    }
    func enterPiP() {
        self.controller?.startPictureInPicture()
    }
    func exitPiP() {
        self.controller?.stopPictureInPicture()
    }
}




class ViewController: UIViewController {
    let streamUrl = URL(string: "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8")!
    let posterUrl = URL(string: "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/poster.jpg")!
    
    //var userInterfaceType: UserInterfaceType = .bitmovin
    var userInterfaceType: UserInterfaceType = .system //added by alberto
    
    var player: Player!
    //var playerView: PlayerView!
    var playerView: CustomPiPView! //added by alberto
    
    deinit {
        player?.destroy()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = .black
        
        let config = createPlayerConfig()
        let player = PlayerFactory.create(playerConfig: config)
        let playerView = createPlayerView(player: player)
        let sourceConfig = createSourceConfig(
            url: streamUrl,
            posterUrl: posterUrl,
            type: .hls
        )
        let source = SourceFactory.create(from: sourceConfig)
        
        player.add(listener: self)
        playerView.add(listener: self)
        
        self.player = player
        //self.playerView = playerView
        self.playerView = createPlayerView(player: player) //added by alberto
        
        view.addSubview(self.playerView)
        
        configureAudioSession()
        
        self.player.load(source: source)
        
        //added by alberto
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            print("entering pip via 5sec automation")
            //self.playerView.enterPictureInPicture()
            self.playerView.enterPiP() 
        }
    }
    
    private func configureAudioSession() {
        // You need to set a category for audio session to '.playback'
        // to be able to use PiP functionality, otherwise PiP won't work
        let audioSession = AVAudioSession.sharedInstance()
        try? audioSession.setCategory(.playback)
    }
    
    private func createPlayerConfig() -> PlayerConfig {
        let config = PlayerConfig()
        config.playbackConfig.isBackgroundPlaybackEnabled = true
        config.playbackConfig.isPictureInPictureEnabled = true
        config.playbackConfig.isAutoplayEnabled = true //added by alberto
        config.styleConfig.isUiEnabled = false //added by alberto
        config.styleConfig.userInterfaceType = userInterfaceType
        
        return config
    }
    
    //added by alberto
    private func createPlayerView(player: Player) -> CustomPiPView {
        let playerView = CustomPiPView(player: player, frame: .zero)
        playerView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        playerView.frame = view.bounds
        
        return playerView
    }
    
    private func createSourceConfig(
        url: URL,
        posterUrl: URL,
        type: SourceType
    ) -> SourceConfig {
        let sourceConfig = SourceConfig(url: streamUrl, type: .hls)
        sourceConfig.posterSource = posterUrl
        return sourceConfig
    }
}

Thanks for your reply @albertobitmovin ! As your solution seems to work when the app is still open, the PiP view disappears/becomes empty whenever the app gets send to background.
When testing the same code on SDK version 3.16.0, the PiP view stays open as intended.
Tested on iOS 16.2

Hi @mart.zonneveld , in order for PiP and Background to work fine combined together, please update these lines in the code I shared above:

isBackgroundPlaybackEnabled = false //doesn't need to be true in this case after 3.17.0 changes
IsPictureInPictureEnabled = false //to use custom PiP rather than Built-in PiP after 3.17.0 changes

With this approach, custom PiP view remains open when app is sent to background. You’ll notice that the stream on PiP view gets automatically paused tho, but this can be easily worked around by programatically calling player.play() on that scenario. I’ve sent you a comprehensive reply via support ticket 11366 as well.

Thanks Alberto, that does seem to do the trick. However we are a bit reluctant to disabling background playback. For now no issues have been found, thus so far this solution will do. Hopefully it won’t be an issue in the future.
Thanks again for your help!

For sake of the Solution to this problem, combine both code samples Alberto provides, including disabling isBackgroundPlaybackEnabled and IsPictureInPictureEnabled