直播TV资源
以下最佳实践、代码示例和其他参考将帮助您更好地了解直播集成在实现阶段的细节。
将程序包添加到许可名单
允许列表确定了哪些应用能够在Fire TV浏览和搜索体验中显示其频道。
最佳实践
以下产品和实施指南将为您的客户提供最佳的Fire TV电视直播体验:
- 提供无冲突注册,鼓励在适用情况下试用。例如简化应用上的注册表单或使用电话号码进行注册。
- 对频道阵容中的每个TvContract.Channels.Logo使用透明的单色标志。
- 优化深层链接流,以在2.5秒内开始全屏播放。
- 操作多个频道时,使用批量操作。
- 在适用情况下利用Amazon Catalog来简化集成。
- 重点关注元数据加载性能优化,而不是提供完整节目单或偏爱的图像大小。
- 使用JobScheduler或WorkManager定期验证授权始终准确。即使您的应用未在前台运行,这也可以确保浏览和搜索中的频道与实际的授权频道始终同步。
- 除了自定义频道排序的情况之外,在授权频道列表稍有变化时,最佳办法是更新现有的授权频道列表,而不是删除并重新添加所有频道。
- 在COLUMN_DISPLAY_NAME中提供将在Fire TV用户界面中显示的频道显示名称。Fire TV最多显示25个字母数字字符,但如果长度超过此限制,则不会显示完整的频道名称。此最大数目限制适用于半角和全角字符集。以下为是否能显示的示例: The Walking Dead Universe(最大长度-通过)/ 简短名称(通过)/ 特别长的电台名(失败)/ 韓流・華流韓流・華流韓(最大长度-通过)。
- 每次执行修改之前,请先查询电视输入框架 (TIF) 数据库,确定数据库内已有哪些频道。
- 插入频道之前,请确保该频道尚不存在。如果该频道已存在,请检查确认元数据是不是最新的。仅当元数据需要更新时,才应该执行数据库更新操作。
- 应检查确认数据库游标为空。如果游标为空,请针对所有带有输入ID的频道发送删除请求,然后重新插入频道。
代码示例
本节包含与直播TV集成相关的代码示例。
使用播放深层链接填充由CDF Station ID标识的频道
TVContractUtils中的以下代码显示了如何将CDF Station ID和深层链接添加到电视数据库中。
/**
 * 用于存储外部ID类型的变量,用于匹配的服务元数据。有效类型为
 * 下面定义为带有前缀“EXTERNAL_ID_TYPE_”的常量
 * 空值或无效数据将导致元数据的服务匹配失败
 */
private final static String EXTERNAL_ID_TYPE = "externalIdType";
 
/**
 * 用于存储外部ID的值的变量,用于匹配的服务元数据。
 * 空值或无效数据将导致元数据的服务匹配失败
 */
private final static String EXTERNAL_ID_VALUE = "externalIdValue";
/**
 * 用于在外部播放器中插入播放的深层链接的URI。
 * 空或无效数据将导致默认与Fire TV原生播放器集成
 */
private final static String PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri";
public void populateChannel(Context context) {
    /**
     * 播放深层链接URI的合约
     * 使用Intent.URI_INTENT_SCHEME从意图创建URI并转换回原始意图
     */
    Intent playbackDeepLinkIntent = new Intent(); // 您的应用为频道创建的实际深层链接意图
    String playbackDeepLinkUri = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME);
    
    /**
     * 构建频道的contentValues 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#") // 替换为您Amazon Catalog的唯一命名空间,例如"test_cdf2"
            .put(EXTERNAL_ID_VALUE, "#Actual Id Value#") // 替换为与频道关联的唯一CDF Station ID,例如"station-001"、"station-child-001"、"station-002"、"station-child-002"、"station-003"、"station-child-003"
            .put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri)
            .toString();
    
        // 将JSON字符串添加到频道的contentValues
        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);
}
fun populateChannel(context: Context) {
        /**
         * 播放深层链接URI的合约
         * 使用Intent.URI_INTENT_SCHEME从意图创建URI并转换回原始意图
         */
        val playbackDeepLinkIntent =
            Intent() // 您的应用为频道创建的实际深层链接意图
        val playbackDeepLinkUri: String = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME)
        /**
         * 构建频道的contentValues BLOB
         */
        val values = ContentValues() // 存储所有频道数据
        val resolver: ContentResolver = context.getContentResolver()
        values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "#Actual display name#")
        values.put(TvContract.Channels.COLUMN_INPUT_ID, "#Actual input id#")
        try {
            val jsonString: String = JSONObject()
                .put(
                    EXTERNAL_ID_TYPE,
                    "#Actual Id Type#"
                ) // 替换为您Amazon Catalog的唯一命名空间,例如"test_cdf2"
                .put(
                    外部ID类型,
                    "#Actual Id Value#"
                ) // 替换为与频道关联的唯一CDF Station ID,例如"station-001"、"station-child-001"、"station-002"、"station-child-002"、"station-003"、"station-child-003"
                .put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri)
                .toString()
            // 将JSON字符串添加到频道的contentValues
            values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.toByteArray())
        } catch (e: JSONException) {
            Log.e(TAG, "Error when adding data to blob $e")
        }
        val uri: Uri = resolver.insert(TvContract.Channels.CONTENT_URI, values)
    }
companion object {
    /**
     * 用于存储外部ID类型的变量,用于匹配的服务元数据。有效类型为
     * 下面定义为带有前缀“EXTERNAL_ID_TYPE_”的常量
     * 空值或无效数据将导致元数据的服务匹配失败
     */
    private const val EXTERNAL_ID_TYPE = "externalIdType"
    /**
     * 用于存储外部ID的值的变量,用于匹配的服务元数据。
     * 空值或无效数据将导致元数据的服务匹配失败
     */
    private const val EXTERNAL_ID_VALUE = "externalIdValue"
    /**
     * 用于在外部播放器中插入播放的深层链接的URI。
     * 空或无效数据将导致默认与Fire TV原生播放器集成
     */
    private const val PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri"
}
使用播放深层链接填充由Gracenote ID标识的频道
TVContractUtils中的以下代码显示了如何将Gracenote ID和深层链接添加到电视数据库中。
/**
 * 用于存储外部ID类型的变量,用于匹配的服务元数据。有效类型为
 * 下面定义为带有前缀“EXTERNAL_ID_TYPE_”的常量
 * 空或无效数据将导致
 * 元数据的服务匹配失败
 */
private final static String EXTERNAL_ID_TYPE = "externalIdType";
/**
 * 用于存储外部ID的值的变量,用于匹配的服务元数据。
 * 空值或无效数据将导致元数据的服务匹配失败
 */
private final static String EXTERNAL_ID_VALUE = "externalIdValue";
/**
 * 用于在外部播放器中插入播放的深层链接的URI。
 * 空或无效数据将导致默认与Fire TV原生播放器集成
 */
private final static String PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri";
// Gracenote输入类型的ID
private final static String GRACENOTE_ID = "gracenote_ontv"; // gracenote ontv id
private final static String GRACENOTE_GVD = "gracenote_gvd"; // gracenote gvd id
// 播放深层链接URI的合约
// 使用Intent.URI_INTENT_SCHEME从意图创建URI并转换回原始意图
Intent playbackDeepLinkIntent = new Intent(); // 由您的应用创建
String playbackDeepLinkUri = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME);
// 构建BLOB
ContentValues values = new ContentValues();  // 存储所有频道数据
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#") // 替换为GRACENOTE_XXX
                  .put(EXTERNAL_ID_VALUE, "#Actual Id Value#") // 替换为与频道关联的gracenote ID值
                  .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
/**
 * 用于存储外部ID类型的变量,用于匹配的服务元数据。有效
 * 类型在下面定义为前缀为"EXTERNAL_ID_TYPE_"的常量空值或无效数据将
 * 导致元数据服务匹配失败
 */
private const val EXTERNAL_ID_TYPE = "externalIdType"
/**
 * 用于存储外部ID的值的变量,用于匹配的服务元数据。空值
 *或无效数据将导致元数据的服务匹配失败
 */
private const val EXTERNAL_ID_VALUE = "externalIdValue"
/**
 * 用于在外部播放器中插入播放的深层链接的URI。空值或无效数据将导致默认
 * 与Fire TV原生播放器集成
 */
private const val PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri"
// Gracenote输入类型的ID
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? {
        // 播放深层链接URI的合约
        // 使用Intent.URI_INTENT_SCHEME从意图创建URI并转换回原始意图
        val playbackDeepLinkIntent = Intent() // 由您的应用创建
        val playbackDeepLinkUri = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME)
        val jsonString: String? = try {
             JSONObject()
                .put(EXTERNAL_ID_TYPE, "#Actual Id Type#") // 替换为GRACENOTE_XXX
                .put(
                    EXTERNAL_ID_VALUE,
                    "#Actual Id Value#"
                ) // 替换为与频道关联的gracenote ID值
                .put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri)
                .toString()
        } catch (e: JSONException) {
            Log.e(TAG, "Error when adding data to blob", e)
            null
        }
        // 构建BLOB
        val values = ContentValues().apply { //存储所有频道数据
            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"
Kepler家长监护
以下代码演示了如何侦听针对直播预览播放的家长监护。
private TvContentRating mBlockedRating = null;
    @Override
    public boolean onTune(final Uri channelUri) {
        ...
        if (mTvInputManager.isParentalControlsEnabled()) {
            // 确保播放在Surface上无法听到或看见
            mBlockedRating = <content_rating>;
            // 1.在全屏播放时为用户触发PIN提示
            // 2.确保浏览On Now行时节目画面
            // 不会跳转到播放Surface。
            notifyContentBlocked(mBlockedRating);
        } else {
            // 播放应开始
            notifyContentAllowed();
        }
        ...
    }
    @Override
    public void onUnblockContent(final TvContentRating unblockedRating) {
        // 用户成功输入PIN以解禁
        // 所选评级对应的内容
        if (unblockedRating.unblockContent(mBlockedRating)) {
            // 播放应开始
            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) {
            // 确保在Surface上无法听到或看到播放
            val blockedRating = getContentRating(channelUri)
            // 1.在全屏播放时为用户触发PIN提示
            // 2.确保浏览On Now行时节目画面
            // 不会跳转到播放Surface。
            notifyContentBlocked(blockedRating)
        } else {
            // 播放应开始
            notifyContentAllowed()
        }
        return true
    }
    override fun onUnblockContent(unblockedRating: TvContentRating) {
        // 用户成功输入PIN以解禁
        // 适用于给定评级的内容
        if (unblockedRating.unblockContent(mBlockedRating)) { // <-- 这是什么?
            // 播放应开始
            notifyContentAllowed();
        }
    }
}
private fun getContentRating(channelUri: Uri): TvContentRating = TODO()
提供应用横幅
要在直播TV设置中显示应用横幅,必须通过程序包管理器提供应用横幅。
// 在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>
要测试横幅,请查看以下代码示例。
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 = "****" //将****替换为程序包名称
val appDrawable: Drawable? = try {
    packageManager.getApplicationBanner(packageName)
} catch (e: PackageManager.NameNotFoundException) {
    Log.i("SetupActivity", "Can't find application banner for package : $packageName")
    null
}
注册BroadcastReceiver以侦听INITIALIZE_PROGRAMS操作
以下代码演示了如何在AndroidManifest.xml中加入一个<receiver>元素,并使用意图筛选器来侦听INITIALIZE_PROGRAMS操作。
<receiver android:name=".InitializationReceiver"
    android:exported="true"
    >
    <intent-filter>
        <action android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />
    </intent-filter>
</receiver>
创建一个扩展BroadcastReceiver的接收器类,并实现onReceive方法。在这里,您可以加入初始化逻辑,仅将授权的频道推送到TIF提供的本地频道数据库中。
public class InitializationReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 此处加入将频道推送到设备上时使用的逻辑
        //注: 这只能用于在安装后将授权频道推送到设备上
    }
}
您可以在示例直播TV应用中找到完整的实现示例。
要测试实现,请选择以下假设之一:
- 假设设备已经连接且APK尚未侧载,则打开一个新终端并使用相应筛选器运行adb logcat以查看与onReceive逻辑相关的所有日志。在单独的终端中运行adb install <APK路径>。安装成功后,相关日志应开始出现,因为添加新电视输入时会广播INITIALIZE_PROGRAMS。
- 假设正在替换APK (apk install -r <APK路径>),则INITIALIZE_PROGRAMS操作不会自动广播。要测试onReceive逻辑,请参阅以下代码片段:
adb shell am broadcast -a android.media.tv.action.INITIALIZE_PROGRAMS -n com.example.android.sampletvinput/.InitializationReceiver
示例直播TV应用
GitHub在github.com/amzn/ftv-livetv-sample-tv-app提供了一个具有直播TV集成的示例应用。这个示例电视应用是基于Google示例电视应用。您可以使用此示例应用作为Fire TV电视直播集成的参考。
仅以下区域设置支持示例应用: 美国、加拿大、英国、德国、日本、西班牙和印度。其他市场即将推出支持。
要加载示例应用,请执行以下操作
- 
    转到https://github.com/amzn/ftv-livetv-sample-tv-app,单击Clone or download(克隆或下载),然后单击Download ZIP(下载ZIP)。解压下载文件。 该应用显示了集成直播TV的示例代码。要查看结果,请使用ADB将app-debug.apk文件侧载到您的Fire TV上,如以下步骤所述。 
- 
    
    如果您已开启调试并安装了ADB,只需前往Settings(设置)> Device & Software(或My Fire TV)(设备和软件,或“我的Fire TV”)> About(关于)> Network(网络)并获取Fire TV的IP地址,然后运行以下内容,从而自定义您自己Fire TV的IP地址: adb connect 123.456.7.89:5555将123.456.7.89替换为您的Fire TV的IP地址。(如果您在连接时遇到问题,并且您正在使用公司VPN,请尝试断开VPN连接,因为您的计算机需要与您的Fire TV处于同一个WiFi网络中。) 
- 
    在示例应用中安装构建的APK。 adb install -r AndroidTvSampleInput/app/build/outputs/apk/app-debug.apk响应如下: Performing Streamed Install Success请注意,此示例应用不会作为传统意义上的独立应用启动。相反,该应用包括了Fire TV设备提供的直播TV频道的代码。 
- 
    在您的Fire TV设备上,前往Settings > Applications(应用)> Manage Installed Applications(管理已安装应用)。选择Sample TV Inputs(电视输入示例)。然后单击启动应用。  Sample TV inputs 这将带您进入亚马逊开发者门户。  亚马逊Fire TV网站 
- 
    在您的Fire TV遥控器上,按下Home(主页)按钮以退出此界面。然后前往Settings > Live TV(直播TV)> Synch Sources(同步源)> Amazon Sample TV Input(亚马逊电视输入示例)。 这将加载示例频道。  同步源 
- 
    同步完成后,按下Home按钮。频道现在应该会在On Now(当前热映)行和指南中显示。 这是On Now行。  Fire TV On Now行 这是Channel Guide(频道指南)。  Fire TV频道指南 要导航到Fire TV上的频道指南,请转到主屏幕,向下滚动到On Now行,按遥控器上的Menu(菜单)按钮,然后点Channel Guide。您也可以按一下遥控器上的麦克风按钮,然后说Channel Guide。 
相关主题
Last updated: 2025年5月5日

