导入TIF Companion库
步骤3: 插入第一个频道
现在介绍如何插入第一个频道。查看下图和Android开发基础知识中的TIF架构示意图。

电视输入会在电视输入框架 (TIF) 数据库中插入频道和节目元数据。Fire TV使用该数据在Fire TV的Live(直播)部分中显示服务的直播内容。电视输入频道和节目元数据必须为最新状态,且与应用内部数据相匹配。步骤3和4将演示如何插入此数据并使其保持最新状态。
向清单中添加权限
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
必须先在AndroidManifest.xml中添加这些权限,然后您的应用才能与TIF数据库交互。
在Android电视数据库中插入频道元数据
在Android电视数据库中插入基本频道的方法有两种:可以在一个类或对象中插入频道。
第一种方法: SetupActivity类
import android.content.ContentValues;
import android.media.tv.TvContract;
import android.util.Log;
import android.net.Uri;
private long insertChannel() {
    ContentValues values = new contentValues();
    values.put(TvContract.Channels.COLUMN_INPUT_ID, "com.example.android.sampletvinput/.RichTvInputService");
    values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "My Test Channel");
    values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, "3");
    Uri uri = getApplicationContext().getContentResolver().insert(TvContract.Channels.CONTENT_URI, values);
    Log.i("SetupActivity", "Channel Inserted! Uri: " + uri);
    long channelId = Long.parseLong(uri.getLastPathSegment());
    return channelId;
}
import android.app.Activity
import android.content.ContentValues
import android.media.tv.TvContract
import android.net.Uri
import android.util.Log
private fun insertChannel(): Long? {
    val values = ContentValues().apply {
        put(TvContract.Channels.COLUMN_INPUT_ID, "com.example.android.sampletvinput/.RichTvInputService")
        put(TvContract.Channels.COLUMN_DISPLAY_NAME, "My Test Channel")
        put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, "3")
    }
    val uri: Uri? = applicationContext.contentResolver.insert(TvContract.Channels.CONTENT_URI, values)
    Log.i("SetupActivity", "Channel Inserted! Uri:$uri")
    return uri?.lastPathSegment?.toLongOrNull()
}
第二种方法: AndroidX库提供的频道对象
import androidx.tvprovider.media.tv.Channel;
import android.media.tv.TvContract;
import android.util.Log;
import android.net.Uri;
private long insertChannel() {
    Channel testChannel = new Channel.Builder()
        .setDisplayName("My Test Channel")
        .setDisplayNumber("3")
        .setInputId("com.example.android.sampletvinput/.RichTvInputService")
        .build();
    Uri uri = getApplicationContext().getContentResolver().insert(TvContract.Channels.CONTENT_URI, testChannel.toContentValues());
    Log.i("SetupActivity", "Channel Inserted! Uri: " + uri);
    long channelId = Long.parseLong(uri.getLastPathSegment());
    return channelId;
}
import android.app.Activity
import android.content.Context
import android.media.tv.TvContract
import android.util.Log
import androidx.tvprovider.media.tv.Channel
private fun insertChannel(): Long? {
    val testChannel = Channel.Builder()
        .setDisplayName("My Test Channel")
        .setDisplayNumber("3")
        .setInputId("com.example.android.sampletvinput/.RichTvInputService")
        .build()
    val uri: Uri? =
        contentResolver.insert(
            TvContract.Channels.CONTENT_URI,
            testChannel.toContentValues()
        )
    Log.i("SetupActivity", "Channel Inserted! Uri:$uri")
    return uri?.lastPathSegment?.toLongOrNull()
}
接下来,在SetupActivity中调用onCreate() 方法(取代现有代码)。
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.rich_setup);
    insertChannel();
}
public override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.rich_setup)
    
    insertChannel()
}
| Activity | 是否必需? | 输入 | 说明 | 
|---|---|---|---|
COLUMN_INPUT_ID | 
    是 | TvInputService的完整类路径 | 示例: TvInputService位于应用主程序包中,完整类路径为<应用程序包>/<TvInputService的相对路径>。 如果TvInputService位于单独的程序包中,则输入ID应为<应用程序包>/<完整单独包+TvInputService的路径>。 | 
  
TvContract.Channels.CONTENT_URI | 
    是 | 这是用来指向Android电视数据库中频道表的URI。 | |
ContentResolver.bulkInsert()或ContentResolver.applyBatch() | 
    是,位于生产代码中 | 以上任意一项都可以确保在一次数据库操作中完成所有频道插入。 | 
插入CDF电台ID
如果您未使用目录数据格式 (CDF),请跳过此部分。
亚马逊的线性目录引入系统是底层技术集成,让您能够将自己的线性频道节目单和节目元数据直接上传到亚马逊Fire TV目录中。如果有兴趣与CDF集成,请联系您的亚马逊代表以了解更多信息。
以下示例展示了如何使用亚马逊合约密钥将唯一频道CDF Station ID插入到JSON对象中,以显示频道类型和ID。可以将其置于SetupActivity中的insert channels函数内。
/**
 * 用于存储外部ID类型的变量,用于匹配的服务元数据。有效类型为
 * 下面定义为带有前缀“EXTERNAL_ID_TYPE_”的常量
 * 空值或无效数据将导致元数据的服务匹配失败
 */
private final static String EXTERNAL_ID_TYPE = "externalIdType";
 
/**
 * 用于存储外部ID的值的变量,用于匹配的服务元数据。
 * 空值或无效数据将导致元数据的服务匹配失败
 */
private final static String EXTERNAL_ID_VALUE = "externalIdValue";
public void fillStationId(ContentValues values) {
   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"
            .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);
    }
}
public fun fillStationId(values: ContentValues) {
    try {
       val jsonString =  JSONObject()
        .put(
            外部ID类型 ,
            "#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"
        .toString()
    
        // 将JSON字符串添加到频道的contentValues
        values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.toByteArray())
    } catch (JSONException e) {
        Log.e(TAG, "Error when adding data to blob " + e)
    }
}
companion object {
    /**
    * 用于存储外部ID类型的变量,用于匹配的服务元数据。有效类型为
    * 下面定义为带有前缀“EXTERNAL_ID_TYPE_”的常量
    * 空值或无效数据将导致元数据的服务匹配失败
    */
    private const val EXTERNAL_ID_TYPE: 字符串   =   "externalIdType"
    
    /**
    * 用于存储外部ID的值的变量,用于匹配的服务元数据。
    * 空值或无效数据将导致元数据的服务匹配失败
    */
    private const val EXTERNAL_ID_VALUE: String = "externalIdValue"
}
| Activity | 是否必需? | 说明 | 
|---|---|---|
externalIdType和externalIdValue | 
    是 | 这些字段名称属于开发者与亚马逊之间合约的一部分,用于向Fire TV提供CDF信息。请勿更改这些字符串。 | 
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA | 
    是 | 这属于开发者与亚马逊之间合约的一部分,用于向Fire TV提供深层链接和CDF信息。 | 
如果尚未设置CDF Station ID,可以暂时使用以下内容进行开发。可以使用示例ID station-001、station-002或station-003,并使用test_cdf2作为提供方命名空间。
插入Gracenote ID
如果没有使用Gracenote,请跳过本节。
Gracenote是与Fire TV集成的电视目录提供方,能够从云端提供频道和节目的元数据。如果您的内容已与Gracenote集成,则可以提供唯一的ID,供Fire TV用来收集元数据。如果有兴趣与Gracenote集成,请联系您的亚马逊代表以了解更多信息。
以下示例展示了如何使用亚马逊合约密钥将唯一频道Gracenote ID插入到JSON对象中,以显示频道类型和ID。可以将其置于SetupActivity中的insert channels函数内。
/**
 * 用于存储外部ID类型的变量,用于匹配的服务元数据。有效类型为
 * 下面定义为带有前缀“EXTERNAL_ID_TYPE_”的常量
 * 空或无效数据将导致
 * 元数据的服务匹配失败
 */
private final static String EXTERNAL_ID_TYPE = "externalIdType";
/**
 * 用于存储外部ID的值的变量,用于匹配的服务元数据。
 * 空值或无效数据将导致元数据的服务匹配失败
 */
private final static String EXTERNAL_ID_VALUE = "externalIdValue";
/**
 * 用于在外部播放器中插入播放的深层链接的URI。
 */
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。
 */
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"
| Activity | 是否必需? | 说明 | 
|---|---|---|
externalIdType和externalIdValue | 
    是 | 这些字段名称属于开发者与亚马逊之间合约的一部分,用于向Fire TV提供Gracenote信息。请勿更改这些字符串。 | 
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA | 
    是 | 这属于开发者与亚马逊之间合约的一部分,用于向Fire TV提供深层链接和Gracenote信息。 | 
- 如果您的Gracenote ID属于其他类型,请查看这是哪个类型。如果您对此不确定,请联系您的亚马逊代表。
 - 
    
如果您计划使用Gracenote,但尚未拥有Gracenote ID,则可以暂时采用以下方式来进行开发。在美国/英国/德国,可以使用以下示例ID: 10171(迪士尼频道)、10240 (HBO)和12131(Cartoon Network),带有gracenote_ontv
externalIdType。对于所有其他市场,可以使用以下示例ID,即带有gracenote_gvdexternalIdType的GN9BBXQSECYVNGW(HBO)。重要须知: 如果频道支持深层链接和Gracenote ID,则应使用上述合约将这二者插入同一JSON对象中。 
插入深层链接
使用亚马逊合约密钥字符串playbackDeepLinkUri将深层链接插入到JSON对象中。
/**
 * 用于在外部播放器中插入播放的深层链接的URI。
 */
private final static String AMZ_KEY_PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri";
...
Intent playbackDeepLinkIntent = new Intent();
...
// 构建频道的contentValues
ContentValues values = new contentValues();
values.put(Channels.COLUMN_INPUT_ID, inputId);
values.put(Channels.COLUMN_DISPLAY_NAME, channel.name);
...
// 构建深层链接Intent
playbackDeepLinkIntent = //提供方频道的深层链接Intent
    ...
    try {
        String jsonString = new JSONObject()
            .put(AMZ_KEY_PLAYBACK_DEEP_LINK_URI, playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME))
            .toString();
        // 将jsonString添加到频道的contentValues
        values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.getBytes());
    } catch (JSONException e) {
        Log.i(TAG, "Error when adding data to blob " + e);
    }
Uri uri = context.getContentResolver().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
/**
 * 用于在外部播放器中插入播放的深层链接的URI。
 */
private const val AMZ_KEY_PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri"
class SetupActivity : Activity() {
    private fun insertChannel(): Long? {
        val playbackDeepLinkIntent = createPlaybackDeepLinkIntent() // 提供方频道的深层链接Intent
        // 构建频道的contentValues
        val values = ContentValues().apply {
            put(
                TvContract.Channels.COLUMN_INPUT_ID,
                "com.example.android.sampletvinput/.RichTvInputService"
            )
            put(TvContract.Channels.COLUMN_DISPLAY_NAME, "My Test Channel")
        }
        try {
            val jsonString = JSONObject()
                .put(
                    AMZ_KEY_PLAYBACK_DEEP_LINK_URI,
                    playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME)
                )
                .toString()
            // 将jsonString添加到频道的contentValues
            values.put(
                TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA,
                jsonString.toByteArray()
            )
        } catch (e: JSONException) {
            Log.i("SetupActivity", "Error when adding data to blob $e")
        }
        val uri = contentResolver.insert(TvContract.Channels.CONTENT_URI, values)
        Log.i("SetupActivity", "Channel Inserted! Uri:$uri")
        return uri?.lastPathSegment?.toLongOrNull()
    }
    private fun createPlaybackDeepLinkIntent(): Intent = TODO()
}
| Activity | 是否必需? | 说明 | 
|---|---|---|
playbackDeepLinkUri | 
    是 | 这属于开发者与亚马逊之间合约的一部分,用于向Fire TV提供频道的深层链接信息。请勿更改此字符串。 | 
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA | 
    是 | 这属于开发者与亚马逊之间合约的一部分,用于向Fire TV提供深层链接和Gracenote信息。 | 
检查点: 在Fire TV的用户界面中显示一个频道
使用以下步骤验证您的设置。
- 在Fire TV上构建并安装您的APK。
 - 导航到Settings(设置)> Live TV(直播TV)> Sync Sources(同步来源),并选择相应的来源。
 - 导航到Home > On Now(当前热映)行。插入的频道应显示为卡片(内容框,有时称为磁贴)之一。如果没有使用Gracenote或CDF进行集成,您将看到一个带有频道名称的灰色磁贴。如果您的设备上有许多来自其他来源的频道,则您的频道可能因为存在限制而无法显示。
 - 导航到Live TV > Channel Guide(频道指南),打开Options(选项)菜单(3行)> Filter Channels(筛选频道)> Your Input Name(您的输入名称)。插入的频道应显示为屏幕上的一行。
 - 导航到Settings > Live TV > Manage Channels(管理频道)。输入名称(来自作业服务XML文件)应显示在列表下方,并且插入的频道应该已被分配给该输入名称。
 - 如果使用深层链接,单击On Now行的频道卡片。此时应用应该启动并显示预期的频道。
 
如果您已集成Gracenote或CDF,频道会在On Now行和Channel Guide中显示完整的节目元数据。
故障排除
此部分包含解决您可能遇到的问题的步骤。
- 频道没有显示在On Now行或Channel Guide中。
 - 
    
- 请参阅检查点以确认是否已在允许列表中添加该频道。
 - 确认频道的输入ID是否与TvInputService的完整类路径等同。
 - 确认调试APK和生产APK是否具有相同的程序包名称。
 - 确认频道是否正确插入到TIF中。
        
- 插入之后,为频道信息立马创建硬编码查询,以确保频道位于数据库中。
 
 - 确认亚马逊能否正确提取该频道。
        
- 
            
插入频道之前,请查看ADB日志:
对于Mac/Linux,请查看
adb logcat | grep StationSync对于Windows,请查看
adb logcat | findstr StationSync - 
            
插入频道后,您应该能够看到类似下文所示的日志。“Added”(已添加)意味着亚马逊正在识别Android电视数据库中的新频道。
 
 - 
            
 
08-07 15:24:57.101 11882 11941 I StationSync: Started full channel sync 08-07 15:24:57.188 11882 11941 I StationSync: Finished full channel sync, found: 15, added: 1, removed: 0, updated: 0 - 频道在On Now行中显示为没有图像的空白磁贴(仅显示频道名称)。
 - 
    
- 如果频道未集成Gracenote或CDF,则此情况属于预期行为。如果已集成Gracenote或CDF,请参阅以下信息。
 
 - 频道具有Gracenote ID或CDF ID,但On Now行或Channel Guide中没有显示元数据。
 - 
    
- 确保您清楚自己的源支持onTV还是GVD或CDF,并在
TvContractUtils中准确定义这一点。Amazon Catalog在某些市场支持onTV。如果亚马逊的支持情况与您拥有的Gracenote ID或CDF ID不匹配,请联系您的亚马逊联系人。他们可能会与Gracenote共同修正该问题,或切换到TIF。 - 重复检查Gracenote ID或CDF ID值。onTV仅使用数字值,而GVD和CDF使用字母数字。
 
注意: 可以参考以下资源来查询Android电视数据库中的现有频道或节目。 - 确保您清楚自己的源支持onTV还是GVD或CDF,并在
 
Last updated: 2025年5月5日

