开发者控制台

步骤3: 插入第一个频道

步骤3: 插入第一个频道

现在介绍如何插入第一个频道。除了这张图表之外,还可以查看Android开发基础知识中的TIF架构图。

频道插入流程

电视输入会在电视输入框架(TIF)数据库中插入频道和节目元数据。此数据将用于在Fire TV直播部分中显示服务的直播内容。电视输入频道和节目元数据必须为最新状态,且与应用内部数据相匹配。步骤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类中添加以下代码: 在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位于单独的程序包中,则inputId应为<应用程序包>/<完整单独包+TvInputService的路径>
TvContract.Channels.CONTENT_URI 这是用来指向Android电视数据库中频道表的URI。
ContentResolver.bulkInsert()ContentResolver.applyBatch() 是,位于生产代码中 以上任意一项都可以确保在一次数据库操作中完成所有频道插入。

插入GracenoteId

如果没有使用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。
 * 空或无效数据将导致默认与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"
Activity 是否必需? 说明
externalIdTypeexternalIdValue 这些字段名称属于开发者与亚马逊之间合约的一部分,用于向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_gvd externalIdTypeGN9BBXQSECYVNGW(HBO)。

如果选择使用深层链接,则使用亚马逊合约密钥字符串playbackDeepLinkUri将深层链接插入到JSON对象中。

/**
 * 用于在外部播放器中插入播放的深层链接的URI。
 * 空值或无效数据将导致默认与GLive TV Native播放器集成
 */
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。空值或无效数据将导致默认
 * 与GLive TV原生播放器集成
 */
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的UI中显示一个频道

  1. 在Fire TV上构建并安装您的APK。
  2. 导航到Settings(设置)> Live TV(直播TV)> Sync Sources(同步来源),并选择相应的来源。
  3. 导航到Home > On Now行。插入的频道应显示为卡片(内容框,有时称为磁贴)之一。如果没有使用Gracenote,则将看到一个带有频道名称的灰色磁贴。如果您的设备上有多个来自其他来源的频道,则设备可能无法显示这些频道(存在限制)。
  4. 导航到Live TV > Channel Guide,打开Options(选项)菜单(3行)> Filter Channels(筛选频道)> Your Input Name(您的输入名称)。插入的频道应显示为屏幕上的一行。
  5. 导航到Settings > Live TV > Manage Channels(管理频道)。输入名称(来自作业服务XML文件)应显示在列表下方,并且插入的频道应该已被分配给该输入名称。
  6. (如果使用深层链接)单击On Now行的频道卡片。此时应用应该启动并显示预期的频道。
  7. (如果已集成Gracenote)频道将在On Now行和Channel Guide中显示完整的节目元数据。

故障排除

频道没有显示在On Now行或Channel Guide中

  • 请参阅“检查点”以确认是否已在允许列表中添加该频道。
  • 确认频道的inputId是否与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,则此情况属于预期行为。如果已集成Gracenote,请参阅以下信息。

频道具有Gracenote ID,但On Now行或Channel Guide中没有显示元数据

  • 确保您清楚自己的源支持onTV还是GVD,并在TvContractUtils中准确定义这一点。Amazon Catalog在某些市场支持onTV。如果亚马逊的支持情况与您拥有的Gracenote ID不匹配,请联系您的亚马逊联系人。他们可能会与Gracenote共同修正该问题,或切换到TIF。
  • 重复检查Gracenote ID值。onTV仅使用数字值,而GVD使用字母数字。

后续步骤

如果没有使用Gracenote,请跳过本步骤: 步骤4: 插入节目

如果正在使用Gracenote,请跳过步骤4,然后转到以下内容: 步骤5: 在Fire TV UI中播放


Last updated: 2022年8月12日