步骤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类
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() |
是,位于生产代码中 | 以上任意一项都可以确保在一次数据库操作中完成所有频道插入。 |
插入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的UI中显示一个频道
- 在Fire TV上构建并安装您的APK。
- 导航到Settings(设置)> Live TV(直播TV)> Sync Sources(同步来源),并选择相应的来源。
- 导航到Home > On Now行。插入的频道应显示为卡片(内容框,有时称为磁贴)之一。如果没有使用Gracenote,则将看到一个带有频道名称的灰色磁贴。如果您的设备上有多个来自其他来源的频道,则设备可能无法显示这些频道(存在限制)。
- 导航到Live TV > Channel Guide,打开Options(选项)菜单(3行)> Filter Channels(筛选频道)> Your Input Name(您的输入名称)。插入的频道应显示为屏幕上的一行。
- 导航到Settings > Live TV > Manage Channels(管理频道)。输入名称(来自作业服务XML文件)应显示在列表下方,并且插入的频道应该已被分配给该输入名称。
- (如果使用深层链接)单击On Now行的频道卡片。此时应用应该启动并显示预期的频道。
- (如果已集成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使用字母数字。
后续步骤
转到下一步: 步骤4: 在Fire TV UI中播放。
Last updated: 2024年7月16日