Developer Console

Live TV Resources

The following best practices, code samples, and other references will help you better understand the specifics of live integration during the implementation phase.

Add Package to Allow List

An allow list determines which applications are capable of surfacing their channels in the Fire TV browse and search.

Best Practices

The following product and implementation guidelines will help provide your customers with the best live TV experience on Fire TV:

  • Provide a friction-free sign-up to encourage trial where applicable. Examples include simplifying registration forms on the application or utilizing a phone number to sign up.
  • Use a transparent, monochrome logo for each TvContract.Channels.Logo in your channel lineup.
  • Optimize the deep-link flow to begin full-screen playback in less than 2.5 seconds.
  • Use bulk actions anytime you are manipulating more than one channel.
  • Leverage Gracenote channel IDs where applicable to simplify integration.
  • Focus on metadata loading performance optimizations over having full schedule availability or preferred image sizes.
  • Use JobScheduler or WorkManager to periodically check and ensure entitlements are always accurate. This will ensure your channels in browse and search are in sync with the entitled channels at all times, even if your application is not in the foreground.
  • With the exception of a custom channel order, update your entitled channel list in place is preferred to removing and re-adding all channels if the entitled channel list has changed slightly.
  • Provide the channel display name in COLUMN_DISPLAY_NAME that will be presented in the Fire TV UI. Fire TV displays up to 25 alphanumeric characters, but won't display the full channel name if the length exceeds this limit. This max limit is applicable for both half-width and full-width character sets. Example of one that won't display: The Walking Dead Universe (Max length-Pass) / Short Name (Pass)/ Extremely Long Station Name (Fail)/ 韓流・華流韓流・華流韓 (Max length-Pass).
  • Before performing any modifications, query the TIF database each time to determine which channels are already in the database.
  • Before inserting a channel, make sure it is not already present. If the channel is already present, check the metadata to see if it is up to date. Only if metadata needs to be updated should a database update operation be executed.
  • The database cursor should be checked to be null. If the cursor is null, send a delete request for all channels with your input ID, and then reinsert channels.

Code Samples

This section contains code samples related to live TV integrations.

The following code from TVContractUtils shows how to include a Gracenote ID and deeplink into the TV database.

/**
 * Variable to store the type of external ID, which is used for the matching service metadata. Valid types are
 * defined below as constants with prefix "EXTERNAL_ID_TYPE_"
 * Null or invalid data will result in failed service
 * match of metadata
 */
private final static String EXTERNAL_ID_TYPE = "externalIdType";

/**
 * Variable to store the value of external ID, which is used for the matching service metadata.
 * Null or invalid data will result in failed service match of metadata
 */
private final static String EXTERNAL_ID_VALUE = "externalIdValue";

/**
 * Uri for deep link of playback into external player.
 * Null or invalid data will result in default as integrated with Fire TV Native Player
 */
private final static String PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri";

// The Id for Gracenote input type
private final static String GRACENOTE_ID = "gracenote_ontv"; // gracenote ontv id
private final static String GRACENOTE_GVD = "gracenote_gvd"; // gracenote gvd id

// Contract for playback deep link uri
// Use Intent.URI_INTENT_SCHEME to create uri from intent and to covert back to original intent
Intent playbackDeepLinkIntent = new Intent(); // Created by your app
String playbackDeepLinkUri = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME);

// Construct BLOB
ContentValues values = new ContentValues();  // store all the channel data
ContentResolver resolver = context.getContentResolver();
values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "#Actual display name#");
values.put(TvContract.Channels.COLUMN_INPUT_ID, "#Actual input id#");
try {
    String jsonString = new JSONObject()
                  .put(EXTERNAL_ID_TYPE, "#Actual Id Type#") // replace with GRACENOTE_XXX
                  .put(EXTERNAL_ID_VALUE, "#Actual Id Value#") // replace with gracenote ID value associated with channel
                  .put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri).toString();

    values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.getBytes());
} catch (JSONException e) {
    Log.e(TAG, "Error when adding data to blob " + e);
}

Uri uri = resolver.insert(TvContract.Channels.CONTENT_URI, values);
import android.app.Activity
import android.content.ContentValues
import android.content.Intent
import android.media.tv.TvContract
import android.util.Log
import org.json.JSONException
import org.json.JSONObject

/**
 * Variable to store the type of external ID, which is used for the matching service metadata. Valid
 * types are defined below as constants with prefix "EXTERNAL_ID_TYPE_" Null or invalid data will
 * result in failed service match of metadata
 */
private const val EXTERNAL_ID_TYPE = "externalIdType"
/**
 * Variable to store the value of external ID, which is used for the matching service metadata. Null
 * or invalid data will result in failed service match of metadata
 */
private const val EXTERNAL_ID_VALUE = "externalIdValue"
/**
 * Uri for deep link of playback into external player. Null or invalid data will result in default
 * as integrated with Fire TV Native Player
 */
private const val PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri"
// The Id for Gracenote input type
private const val GRACENOTE_ID = "gracenote_ontv" // gracenote ontv id
private const val GRACENOTE_GVD = "gracenote_gvd" // gracenote gvd id



class SetupActivity : Activity() {
    private fun insertChannel(): Long? {
        // Contract for playback deep link uri
        // Use Intent.URI_INTENT_SCHEME to create uri from intent and to covert back to original intent
        val playbackDeepLinkIntent = Intent() // Created by your app
        val playbackDeepLinkUri = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME)

        val jsonString: String? = try {
             JSONObject()
                .put(EXTERNAL_ID_TYPE, "#Actual Id Type#") // replace with GRACENOTE_XXX
                .put(
                    EXTERNAL_ID_VALUE,
                    "#Actual Id Value#"
                ) // replace with gracenote ID value associated with channel
                .put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri)
                .toString()
        } catch (e: JSONException) {
            Log.e(TAG, "Error when adding data to blob", e)
            null
        }

        // Construct BLOB
        val values = ContentValues().apply { // store all the channel data
            put(TvContract.Channels.COLUMN_DISPLAY_NAME, "#Actual display name#")
            put(TvContract.Channels.COLUMN_INPUT_ID, "#Actual input id#")
            if (jsonString != null) {
                put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.toByteArray())
            }
        }

        val uri = contentResolver.insert(TvContract.Channels.CONTENT_URI, values)

        Log.i("SetupActivity", "Channel Inserted! Uri: $uri")
        return uri?.lastPathSegment?.toLongOrNull()
    }
}

private val TAG = "MyTAG"

Honoring Parental Controls

The following code demonstrates how to listen to parental controls for 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>;
            // 1. Triggers PIN prompt to user when in fullscreen playback
            // 2. Ensures the program image does not flip to the playback Surface
            //    when browsing the On Now row.
            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)
            // 1. Triggers PIN prompt to user when in fullscreen playback
            // 2. Ensures the program image does not flip to the playback Surface
            //    when browsing the On Now row.
            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(mBlockedRating)) { // <-- What is this?
            // playback should start
            notifyContentAllowed();
        }
    }
}

private fun getContentRating(channelUri: Uri): TvContentRating = TODO()

Providing an Application Banner

In order to show your application banner in Live TV settings, you will need to provide an application banner through the package manager.

// in AndroidManifest.xml
<application
    android:allowBackup="false"
    android:label="@string/app_name"
    android:banner="@drawable/app_icon_banner"
    tools:replace="android:allowBackup, allow:label, android:theme" >

    <meta-data
        android:name="****"
        android:value="true"
    />
</application>

To test the banner, refer to this code snippet:

Drawable appDrawable = null;
try {
    String packageName = "****"; // replace **** with real package name
    PackageManager packageManager = getContext().getPackageManager();
    appDrawable = packageManager.getApplicationBanner(packageName);
} catch (PackageManager.NameNotFoundException e) {
    Log.i(TAG, "Can't find application banner for package : " + packageName);
}
val packageName = "****" // replace **** with real package name
val appDrawable: Drawable? = try {
    packageManager.getApplicationBanner(packageName)
} catch (e: PackageManager.NameNotFoundException) {
    Log.i("SetupActivity", "Can't find application banner for package : $packageName")
    null
}

Sample Live TV App

A sample app with live TV integration is available on GitHub at github.com/amzn/ftv-livetv-sample-tv-app. This sample TV app is based on the Google sample TV app. You can use this sample app as a reference for the live TV integration on Fire TV.

Locale Support for Live Apps

The sample app is supported only in the following locales: US, CA, UK, DE, JP, ES, and IN. Other marketplace support is coming soon.

To load the sample app:

  1. Go to https://github.com/amzn/ftv-livetv-sample-tv-app, click Clone or download, and then click Download ZIP. Unzip the download.

    The app shows sample code for integrating live TV. To see the result, use ADB to sideload the app-debug.apk file onto your Fire TV, as described in the following steps.

  2. Connect to Fire TV through ADB.

    If you've already turned on debugging and have adb installed, just get your Fire TV's IP address from Settings > Device & Software (or My Fire TV) > About > Network and run the following, customizing the IP address for your own Fire TV:

    adb connect 123.456.7.89:5555
    

    Replace 123.456.7.89 with your Fire TV's IP address. (If you have trouble connecting and you're on your corporate VPN, try disconnecting from VPN, since your computer needs to be on the same wifi network as your Fire TV.)

  3. Install the built APK in the sample app:

    adb install -r AndroidTvSampleInput/app/build/outputs/apk/app-debug.apk
    

    The response is as follows:

    Performing Streamed Install
    Success
    

    Note that this sample app does not launch as a standalone app in the traditional sense. Instead, it incorporates code for live TV channels available from the Fire TV device.

  4. On your Fire TV device, go to Settings > Applications > Manage Installed Applications. Select Sample TV Inputs. Then click Launch application.

    Sample TV inputs
    Sample TV inputs

    This will take you to the Amazon developer portal.

    Amazon Fire TV site
    Amazon Fire TV site
  5. On your Fire TV remote, click the Home button to back out of this screen. Then go to Settings > Live TV > Sync Sources > Amazon Sample TV Input.

    This will load the sample channels.

    Syncing sources
    Syncing sources
  6. After the sync finishes, click the Home button. Channels should now be visible in the "On Now" row and Guide.

    Here's the "On Now" row:

    Fire TV 'On Now' row
    Fire TV 'On Now' row

    Here's the Channel Guide.

    Fire TV Channel Guide
    Fire TV Channel Guide

    To navigate to the Channel Guide on Fire TV, go to your Home screen, scroll down to the "On Now" row, press the menu button on your remote, and then click Channel Guide. You can also click the mic button on your remote and say, "Channel Guide."

Customized Log Debugging

How to Find All Your Existing Channels

Here’s an example of how to query Android’s TV Database for all existing channels.

private static String[] CHANNEL_TABLE_PROJECTIONS = new String[] {
    TvContractCompat.Channels._ID,
        TvContractCompat.Channels.COLUMN_DESCRIPTION,
        TvContractCompat.Channels.COLUMN_DISPLAY_NAME,
        TvContractCompat.Channels.COLUMN_DISPLAY_NUMBER,
        TvContractCompat.Channels.COLUMN_INPUT_ID,
        TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_DATA,
        TvContractCompat.Channels.COLUMN_NETWORK_AFFILIATION,
        TvContractCompat.Channels.COLUMN_ORIGINAL_NETWORK_ID,
        TvContractCompat.Channels.COLUMN_PACKAGE_NAME,
        TvContractCompat.Channels.COLUMN_SEARCHABLE,
        TvContractCompat.Channels.COLUMN_SERVICE_ID,
        TvContractCompat.Channels.COLUMN_SERVICE_TYPE,
        TvContractCompat.Channels.COLUMN_TRANSPORT_STREAM_ID,
        TvContractCompat.Channels.COLUMN_TYPE,
        TvContractCompat.Channels.COLUMN_VIDEO_FORMAT,
        TvContractCompat.Channels.COLUMN_BROWSABLE,
        TvContractCompat.Channels.COLUMN_LOCKED,
        TvContractCompat.Channels.COLUMN_APP_LINK_COLOR,
        TvContractCompat.Channels.COLUMN_APP_LINK_ICON_URI,
        TvContractCompat.Channels.COLUMN_APP_LINK_INTENT_URI,
        TvContractCompat.Channels.COLUMN_APP_LINK_POSTER_ART_URI,
        TvContractCompat.Channels.COLUMN_APP_LINK_TEXT,
        TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1,
        TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
        TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_FLAG3,
        TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_FLAG4
};

public List < Channel > getChannels(ContentResolver resolver) {
    Log.d(Utils.DEBUG_TAG, "Start testing query channel table for preview channels...");
    Cursor cursor = null;
    List < Channel > channels = new ArrayList < > ();
    try {
        cursor = resolver.query(TvContractCompat.Channels.CONTENT_URI, CHANNEL_TABLE_PROJECTIONS, null, null, null);
        if (cursor == null || cursor.getCount() == 0) {
            Log.d(TAG, "No channel inserted \n");
            return null;
        }
        while (cursor.moveToNext()) {
            Channel channel = Channel.fromCursor(cursor);
            channels.add(channel);
            Log.d(TAG, "Found channel " + channel);
        };
    } catch (Exception e) {
        Log.d(TAG, "Unable to get the channels " + e);
        return null;
    } finally {
        if (cursor != null) {
            cursor.close();

        }
    }
    return channels;
}
private val CHANNEL_TABLE_PROJECTIONS = arrayListOf(
    TvContractCompat.Channels._ID,
    TvContractCompat.Channels.COLUMN_DESCRIPTION,
    TvContractCompat.Channels.COLUMN_DISPLAY_NAME,
    TvContractCompat.Channels.COLUMN_DISPLAY_NUMBER,
    TvContractCompat.Channels.COLUMN_INPUT_ID,
    TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_DATA,
    TvContractCompat.Channels.COLUMN_NETWORK_AFFILIATION,
    TvContractCompat.Channels.COLUMN_ORIGINAL_NETWORK_ID,
    TvContractCompat.Channels.COLUMN_PACKAGE_NAME,
    TvContractCompat.Channels.COLUMN_SEARCHABLE,
    TvContractCompat.Channels.COLUMN_SERVICE_ID,
    TvContractCompat.Channels.COLUMN_SERVICE_TYPE,
    TvContractCompat.Channels.COLUMN_TRANSPORT_STREAM_ID,
    TvContractCompat.Channels.COLUMN_TYPE,
    TvContractCompat.Channels.COLUMN_VIDEO_FORMAT,
    TvContractCompat.Channels.COLUMN_BROWSABLE,
    TvContractCompat.Channels.COLUMN_LOCKED,
    TvContractCompat.Channels.COLUMN_APP_LINK_COLOR,
    TvContractCompat.Channels.COLUMN_APP_LINK_ICON_URI,
    TvContractCompat.Channels.COLUMN_APP_LINK_INTENT_URI,
    TvContractCompat.Channels.COLUMN_APP_LINK_POSTER_ART_URI,
    TvContractCompat.Channels.COLUMN_APP_LINK_TEXT,
    TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1,
    TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
    TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_FLAG3,
    TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_FLAG4
)

class SetupActivity : Activity() {
   ...

    private fun getChannels(): List<Channel> {
        Log.d(TAG, "Start testing query channel table for preview channels...")
        return try {
            val cursor = contentResolver.query(
                TvContractCompat.Channels.CONTENT_URI,
                CHANNEL_TABLE_PROJECTIONS,
                null,
                null,
                null
            )

            cursor.use {
                if (cursor == null || cursor.count == 0) {
                    Log.d(TAG, "No channel inserted \n")
                    emptyList()
                } else {
                    buildList {
                        while (cursor.moveToNext()) {
                            val channel = Channel.fromCursor(cursor)
                            Log.d(TAG, "Found channel $channel")
                            add(channel)
                        }
                    }
                }
            }
        } catch (e: Exception) {
            Log.d(TAG, "Unable to get the channels", e)
            emptyList()
        }
    }
}

How to Find All Your Programs in a Channel

Here is how you query for existing programs.

Code Sample: Example of querying program metadata belonging to a specific channel:

public static List < Program > getPrograms(ContentResolver resolver, Uri channelUri) {
    if (channelUri == null) {
        return null;
    }
    Uri uri = TvContract.buildProgramsUriForChannel(channelUri);
    List < Program > programs = new ArrayList < > ();
    // TvProvider returns programs in chronological order by default.
    Cursor cursor = null;
    try {
        cursor = resolver.query(uri, Program.PROJECTION, null, null, null);
        if (cursor == null || cursor.getCount() == 0) {
            return programs;
        }
        while (cursor.moveToNext()) {
            programs.add(Program.fromCursor(cursor));
        }
    } catch (Exception e) {
        Log.w(TAG, "Unable to get programs for " + channelUri, e);
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
    return programs;
}
fun getPrograms(resolver: ContentResolver, channelUri: Uri): List<Program> {
    val uri = TvContract.buildProgramsUriForChannel(channelUri)

    return try {
        // TvProvider returns programs in chronological order by default.
        val cursor = resolver.query(uri, null, null, null, null) // <- Program.PROJECT causes error, null means all.
        cursor.use {
            if (cursor == null || cursor.count == 0) {
                emptyList()
            } else {
                buildList {
                    while (cursor.moveToNext()) {
                        add(Program.fromCursor(cursor))
                    }
                }
            }
        }
    } catch (e: Exception) {
        Log.w(TAG, "Unable to get programs for $channelUri", e)
        emptyList()
    }
}

Last updated: Oct 10, 2022