Developer Console

Step 4: Playback in Fire TV UI

Live TV has the ability to preview playback whenever a customer focuses on one of the tiles in a browse row. This is a convenient and quick way to preview content.

Playback flow

Integration with preview playback requires that you play channel content within a player that can use a Surface provided by the live TV app.

Create TvInputService.Session

When a user selects a specific channel, the TvInputService class is called to create a Session. The session is where you can choose the content, prepare the player, and render channel content. Add this into the TvInputService class, RichTvInputService.

The following code shows an example of a concrete Session class.

private class PreviewSession extends TvInputService.Session {
    PreviewSession(Context context) {
        super(context);
        Log.d(Utils.DEBUG_TAG, "session created!");
    }

    @Override
    public boolean onTune(Uri channelUri) {
        Log.d(Utils.DEBUG_TAG, "onTune " + channelUri);

        ...
        return false;
    }

    @Override
    public boolean onSetSurface(@Nullable Surface surface) {
        Log.d(Utils.DEBUG_TAG, "onSetSurface");
        ...
        return false;
    }
}
import android.content.Context
import android.media.tv.TvInputService
import android.net.Uri
import android.util.Log
import android.view.Surface

private const val TAG = "MyTag"

private class PreviewSession(context: Context) : TvInputService.Session(context) {

    init {
        Log.d(TAG, "session created!")
    }

    override fun onTune(channelUri: Uri): Boolean {
        Log.d(TAG, "onTune $channelUri")

        return false
    }

    override fun onSetSurface(surface: Surface?): Boolean {
        Log.d(TAG, "onSetSurface")

        return false
    }

    override fun onSetCaptionEnabled(enabled: Boolean) {
        TODO("Not yet implemented")
    }

    override fun onRelease() {
        TODO("Not yet implemented")
    }

    override fun onSetStreamVolume(volume: Float) {
        TODO("Not yet implemented")
    }
}

Identify the correct channel

You can identify the current channel users are tuning to through the onTune() callback by the channelId.

The following code shows an example of how to get the channel ID from the channelUri.

long channelId = Long.parseLong(channelUri.getLastPathSegment());
import android.net.Uri

val channelUri: Uri = TODO()
var channelId: Long? = channelUri.lastPathSegment?.toLong()

The channel ID is the ID that's auto-assigned by Android during the channel insertion into Android's TV database. You must keep a map between the channel ID Android assigns when the channel is inserted into the TV database, and your channel ID. This way, you can always find the correct channel content using the channel ID.

Play the channel content in preview playback

Implement a media player that can play your TV feed on a configurable surface.

When a user moves the focus to a specific channel card when browsing, the live app uses the session from the previous step. Your app defines the session as part of the TvInputService.Session class, and uses it to tune to the requested channel and play content.

Create a media player

There are several options for implementing a media player. You must have a well-defined media player inside your app that you can directly use. There is no hard requirement as to which media player you must use, as long as the player can play your TV feeds and you can configure it to use a customized Surface class. For details, see #onSetSurface. Because of this case-by-case player implementation, the following options are provided for reference only.

ExoPlayer: a good candidate for the underlying media player.

SampleTvApp's DemoPlayer: an example of using ExoPlayer to construct a media player that supports TV channel tunings.

SampleTvApp's DemoPlayer in TvInputService: an example of how to define the customized media player in TvInputService.

onSetSurface

During the tuning process, Android's TIF framework calls the onSetSurface(@Nullable Surface surface) callback, which is defined in the TvInputService.Session (refer to previous steps). It's your responsibility to set the provided Surface instance to your media player, which is used for preview playback.

onTune

The onTune(Uri channelUri) callback is called next, where you identify the correct channel (refer to Identify the Correct Channel), retrieve the corresponding channel feed, and prepare the media player to play the feed when ready.

Here are some typical scenarios for not calling onTune() for the second playback:

  1. When the user moves focus to a channel card, onTune() calls for preview playback.
  2. When the user clicks the current card and enters the full-screen playback.
  3. When full-screen playback seamlessly displays full-screen. You won't receive another onTune() in this case.

Notify tuning status

You must notify TvInputService about the most recent tuning status based on the player's loading status. Fire TV's Live app refers to the status to adjust the preview UI.

Here is an example of notifying the tuning status. In this case, the status is "temporarily unavailable." Place this code in TvInputService.Session.

@Override
public boolean onTune(Uri channelUri) {
    Log.d(Utils.DEBUG_TAG, "onTune " + channelUri);
    // Let the TvInputService know that the video is being loaded.
    notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_TUNING);
}
override fun onTune(channelUri: Uri): Boolean {
    Log.d(TAG, "onTune $channelUri")
    notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_TUNING)
   return true
}

The following code shows an example of a notification of video availability status.

notifyTracksChanged(getAllTracks());
String audioId = getTrackId(TvTrackInfo.TYPE_AUDIO,
    mPlayer.getSelectedTrack(TvTrackInfo.TYPE_AUDIO));
String videoId = getTrackId(TvTrackInfo.TYPE_VIDEO,
    mPlayer.getSelectedTrack(TvTrackInfo.TYPE_VIDEO));
String textId = getTrackId(TvTrackInfo.TYPE_SUBTITLE,
    mPlayer.getSelectedTrack(TvTrackInfo.TYPE_SUBTITLE));


notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, audioId);
notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, videoId);
notifyTrackSelected(TvTrackInfo.TYPE_SUBTITLE, textId);
notifyVideoAvailable();

val audioId: String = getTrackId(
    TvTrackInfo.TYPE_AUDIO,
    mPlayer.getSelectedTrack(TvTrackInfo.TYPE_AUDIO)
)
val videoId: String = getTrackId(
    TvTrackInfo.TYPE_VIDEO,
    mPlayer.getSelectedTrack(TvTrackInfo.TYPE_VIDEO)
)
val textId: String = getTrackId(
    TvTrackInfo.TYPE_SUBTITLE,
    mPlayer.getSelectedTrack(TvTrackInfo.TYPE_SUBTITLE)
)

notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, audioId)
notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, videoId)
notifyTrackSelected(TvTrackInfo.TYPE_SUBTITLE, textId)
notifyVideoAvailable()

Parental controls

Based on the product requirements, don't play preview playback videos if parental controls (PCON) is enabled.

The following code shows an example of how to listen to parental controls with live preview or native full screen playback.

private TvContentRating mBlockedRating = null;

@Override
public boolean onTune(final Uri channelUri) {
    ...
    if (mTvInputManager.isParentalControlsEnabled()) {
        // ensure playback is not audible or visible on the Surface
        mBlockedRating = < content_rating > ;
        notifyContentBlocked(mBlockedRating);
    } else {
        // playback should start
        notifyContentAllowed();
    }
    ...
}

@Override
public void onUnblockContent(final TvContentRating unblockedRating) {
    // the user successfully entered their PIN to unblock content for the
    // provided rating
    if (unblockedRating.unblockContent(mBlockedRating)) {
        // playback should start
        notifyContentAllowed();
    }
}
import android.content.Context
import android.media.tv.TvContentRating
import android.media.tv.TvInputManager
import android.media.tv.TvInputService
import android.net.Uri
import android.view.Surface

private const val TAG = "MyTag"

private class PreviewSession(context: Context) : TvInputService.Session(context) {

    private val tvInputManager: TvInputManager = TODO()



    override fun onTune(channelUri: Uri): Boolean {
        if (tvInputManager.isParentalControlsEnabled) {
            // ensure playback is not audible or visible on the Surface
            val blockedRating = getContentRating(channelUri)
            notifyContentBlocked(blockedRating)
        } else {
            // playback should start
            notifyContentAllowed()
        }
        return true
    }


    override fun onUnblockContent(unblockedRating: TvContentRating) {
        // the user successfully entered their PIN to unblock content for the
        // provided rating
        if (unblockedRating.unblockContent(blockedRating)) { // <-- What is this?
            // playback should start
            notifyContentAllowed()
        }
    }

}

private fun getContentRating(channelUri: Uri): TvContentRating = TODO()
Activity Required? Notes
mTvInputManager.isParentalControlsEnabled() Yes This method is called to check PCON status.
notifyContentBlocked() Depends This method should be called whenever the video is blocked from playing because of PCON.
notifyContentAllowed() Depends This method should be called whenever the video is good to play.

Checkpoint: Preview playback in browse

  1. Build and install your APK on a Fire TV.
  2. Navigate to the On Now row and focus on a channel card. Preview playback should start to play in the top right corner.
    • If not using deep link: select channel card and playback should continue in full screen.
  3. Navigate to the parental control menu to turn on parental controls.
  4. Navigate back to the On Now row and focus on a channel card. Preview playback should NOT start to play, but poster art should show up on the browse screen if provided.
    • If not using deep link: select channel card and a PIN prompt appears. Insert the PIN for playback to start in full screen.

Troubleshooting

This section contains steps to troubleshoot issues you might encounter.

I don't see the onTune() callback being triggered when focusing on my channel card.
Verify the input ID of that channel. Android won't identify your TvInputService to call it if the InputId is not correct.
I can see onTune() being called, but the playback preview doesn't start.
  1. Check if your player is correctly implemented for playing the feed.
  2. Ensure you are calling notifyVideoAvailable() to notify that your tuning status is ready.
After enabling parental controls (PCON), I see the preview playback still playing in the browse section.
Verify you are implementing the Parental Control code correctly in your TvInputService. Your player should stop if PCON is on, and you use notifyContentBlocked() to notify the UI. For more information, see Live TV Resources.
After enabling parental controls (PCON), I don't see the preview playback nor the poster image. I only see a black background.
Make sure your channel has valid poster art to show. It should show the image of the program currently airing.

If it is, verify that you are calling notifyContentBlocked(). The UI won't update to use the poster image if you don't send a notification about the PCON status.



Last updated: May 05, 2025