How to manage focus handling in AVPlayerViewController with custom controls?

760 Views Asked by At

I try to add a skip-intro-button (like Netflix) to the AVPlayerViewController of a tvOS movie app. I added it as subview to the contentOverlayView and when I let it appear I force giving it the focus with preferredFocusEnvironments. Everything's fine... Until the user navigates somewhere else (e.g. seek bar or video asset info view). The button then loses focus (expected) and never can be focused again by user interaction. I tried to:

  • add a UIFocusGuide()
  • put my button directly on the AVPlayerViewController's view
  • add a own subview, which contains the buttons to the AVPlayerViewController's view
  • add a own subview, which contains the buttons to the contentOverlayView
  • add several other buttons next to, below, above my button on the same subview (for each of the cases above)

The last approach shows that none of the other buttons can ever get focus by user interaction, so it seems, that, for the same reason, the skip-intro-button cannot be focused by the user. But what is this reason? What is the right practice to add custom interaction elements to AVPlayerViewController? Anyone any ideas?

1

There are 1 best solutions below

1
On BEST ANSWER

Try to answer my main question "What is the right practice to add custom interaction elements to AVPlayerViewController?" myself:

Since I posted this question, 1.5 years ago, I didn't change the implementation of the skip intro feature very much. So whenever the user uses the remote, the button will lose focus and the only thing I changed is, that I hide the button whenever this happens, by implementing didUpdateFocus(in:with:), similar to this:

if let previouslyFocusedView = context.previouslyFocusedView {
    if previouslyFocusedView == skipIntroButton {
        changeSkipIntroButtonVisibility(to: 0.0)//animates alpha value
    }
}

(I'm not completely sure, why I don't set it to isHidden = true)

However, in the meantime I had to implement a more complex overlay to our player, i.e. a "Start Next Video / Watch Credits" thing, with a teaser, some buttons, a countdown/progress bar and more. With the problems described above, it is obvious, that I couldn't go with the contentOverlayView approach. So I decided to implement it the "traditional way", by presenting a complete UIViewController on top of the player's view, like this:

func showNextVideoOverlay() {
    guard let nextVideoTeaser = nextVideoTeaser else { return }
    let nextVideoOverlay = NextVideoAnnouncementViewController(withTeaser: nextVideoTeaser, player: player)
    nextVideoOverlay.nextVideoAnnouncementDelegate = self
    nextVideoOverlay.modalPresentationStyle = .overFullScreen
    present(nextVideoOverlay, animated: true, completion: nil)
}

Of course the NextVideoAnnouncementViewController is transparent, so video watching is still possible. I turned out that this straight forward approach works pretty well and I really don't know, why I haven't thought about it, when implementing skip intro.

My colleagues in QA found one tricky thing, you should be aware of (and I think, I remember, that this is different with different Devices and different remotes and on different tvOS versions - try it):

The overlying view controller blocks most of the commands coming from the remote, respectively you can navigate inside the view controller without affecting the player - except the play/pause button. That one will pause the player, but it's not possible to resume from there. This is, because a playing (av)player always listens to this button, while a paused one doesn't. So I also had to implement something like this:

func addRemoteButtonRecognizer() {
    let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(playPauseButtonPressed))
    tapRecognizer.allowedPressTypes = [NSNumber(value: UIPress.PressType.playPause.rawValue)]
        self.view.addGestureRecognizer(tapRecognizer)
}

@objc func playPauseButtonPressed(sender: AnyObject) {
    nextVideoAnnouncementDelegate?.remotePlayButtonPressed()
}

func remotePlayButtonPressed() {
    if player?.playerStatus == .paused {
        player?.play()
    } else {
        player?.pause()
    }
}

I hope this helps some of you, who come by here and find no other answer.