Custom tvOS player

I have been working on a custom player control for tvOS recently for one of our project. In this little article I would like to share what are the important interactions to replicate if you are implementing a custom player and not using the default AVPlayerView Controller

Interactions with Gesture Recognizers

4 types of gesture recognizers are important to implement in your app.

  • UITapGestureRecognizer (for click/tap)
  • UILongPressGestureRecognizer (long press on buttons)
  • UIPanGestureRecognizer (panning on Siri Remote)
  • UISwipeGestureRecognizer (Swipes)

See the apple documentation for more details.

Info panel

Info panel is usually appearing from the top of the screen, and displayed as an overlay as soon as you perform the following gestures:

  • Tap down
  • Swipe down

Reveal controls

If you just want to see the controls, you can use the following gestures:

  • Tap up
  • Swipe up

Play Pause

If the player is playing, and the user clicks the select button (Tap select), the player is supposed to pause, and controls should reveal.

If the user clicks on the play/pause (Tap play/pause) button, the player should toggle playback.

Fast forward, fast rewind

Using the DPad, you are able to perform fast forward and fast rewind. This is implemented by changing the playback rate on the player, switching from 1 to 8 (1x) to 24 (2x) to 48 (3x) to 96 (4x)

  • Initiate the fast forward with Long Press right
  • Tap right once more, and playback rate increase to next level
  • If rate is 96, and user presses once more, the playback goes back to normal playback
  • Same applies with left button for fast rewind

Quick Seek

It is recommended to have a way for quick seek, when the player is on pause or playing normally.

  • Tap Left, should seek back 10-15 seconds
  • Tap right, should seek forward 10-15 seconds

Seek on bar

When the player is on pause, it should be possible to seek along the transport bar using pan on the bar.

  • Pan left/right should move the cursor left of right
  • Circular pan (only new SIRI remote from 2021) should seek along the bar. Though this is not directly supported by Apple API, you may need to implement the gesture yourself.

PageUp / PageDown

Some of the remote controls have a page up/ page down button. Since tvOS 14.3, the UITapGestureRecognizer allows these interactions.

It is recommended to use these button to change channel on LIVE tv

  • Tap Page up: Change to next channel
  • Tap Page down: Change to previous channel

Interactions with MPRemoteCommands

Apple provides another API to interact with the fast forward and fast rewind like using the DPad. You might connect your player with the MPRemoteCommands as follow

  • seekForwardCommand and seekBackwardCommand will be triggered on long press on the remote control buttons
  • changePlaybackRateCommand will be trigger each time the user single press on the remote control buttons

Seek Commands

These command will come with a begin and an end even state.

  • On begin state, change the playback rate to a higher value (like 24)
  • On end state, change back the rate to 1 (to play normally)
let remoteCommandCenter = MPRemoteCommandCenter.shared()
remoteCommandCenter.seekForwardCommand.addTarget { [unowned self] event in
     guard let event = event as? MPSeekCommandEvent else { return .commandFailed }
     if event.type == .beginSeeking {
         self.assetPlayer.player.rate = 24.0
     if event.type == .endSeeking {
         self.assetPlayer.player.rate = 1.0
     return .success

Change Playback Rate Command

This command will be triggered when the user single click on the button. therefore the behaviour will be the same as using the DPad moving from 1, 8, 24, 48, 96 rate. The rate to use will be given by the command event itself as in the example.

let remoteCommandCenter = MPRemoteCommandCenter.shared()
 remoteCommandCenter.changePlaybackRateCommand.addTarget { [unowned self] event in
     guard let event = event as? MPChangePlaybackRateCommandEvent else { return .commandFailed }
     self.assetPlayer.player.rate = event.playbackRate
     return .success

⚠️ It is very important to update the NowOnPlaying Info right after the playback rate is updated to the player, otherwise your might have the wrong playback rate coming from apple API.

Thank you for reading until here…  👊 Have a good week end…  🍺