您的电视输入必须在其设置 Activity 中为至少一个频道提供电子节目指南 (EPG) 数据。您还应该定期更新这些数据,同时考虑更新的大小以及处理这些数据的线程。此外,您可以为频道提供应用链接,以引导用户访问相关内容和 Activity。本课程将讨论在系统数据库上创建和更新频道及节目数据时需要考虑的这些事项。
请试用 电视输入服务 示例应用。
获取权限
为了让您的电视输入使用 EPG 数据,它必须在其 Android 清单文件中声明写入权限,如下所示:
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
在数据库中注册频道
Android TV 系统数据库维护电视输入的频道数据记录。在您的设置 Activity 中,对于您的每个频道,您必须将频道数据映射到 TvContract.Channels
类的以下字段:
COLUMN_DISPLAY_NAME
- 频道的显示名称COLUMN_DISPLAY_NUMBER
- 显示的频道号COLUMN_INPUT_ID
- 电视输入服务的 IDCOLUMN_SERVICE_TYPE
- 频道的服务类型COLUMN_TYPE
- 频道的广播标准类型COLUMN_VIDEO_FORMAT
- 频道的默认视频格式
尽管电视输入框架足够通用,可以不加区分地处理传统广播和 OTT(over-the-top)内容,但您可能需要在上述列之外定义以下列,以更好地识别传统广播频道:
COLUMN_ORIGINAL_NETWORK_ID
- 电视网络 IDCOLUMN_SERVICE_ID
- 服务 IDCOLUMN_TRANSPORT_STREAM_ID
- 传输流 ID
如果您想为频道提供应用链接详细信息,您需要更新一些额外的字段。有关应用链接字段的更多信息,请参阅添加应用链接信息。
对于基于互联网流媒体的电视输入,请相应地为上述字段分配您自己的值,以便每个频道都能被唯一标识。
从您的后端服务器拉取频道元数据(XML、JSON 或其他格式),并在您的设置 Activity 中将值映射到系统数据库,如下所示:
Kotlin
val values = ContentValues().apply { put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.number) put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.name) put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId) put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId) put(TvContract.Channels.COLUMN_SERVICE_ID, channel.serviceId) put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.videoFormat) } val uri = context.contentResolver.insert(TvContract.Channels.CONTENT_URI, values)
Java
ContentValues values = new ContentValues(); values.put(Channels.COLUMN_DISPLAY_NUMBER, channel.number); values.put(Channels.COLUMN_DISPLAY_NAME, channel.name); values.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId); values.put(Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId); values.put(Channels.COLUMN_SERVICE_ID, channel.serviceId); values.put(Channels.COLUMN_VIDEO_FORMAT, channel.videoFormat); Uri uri = context.getContentResolver().insert(TvContract.Channels.CONTENT_URI, values);
在上面的示例中,channel
是一个保存来自后端服务器的频道元数据的对象。
呈现频道和节目信息
当用户切换频道时,系统电视应用会向用户呈现频道和节目信息,如图 1 所示。为确保频道和节目信息与系统电视应用的频道和节目信息呈现器兼容,请遵循以下准则。
- 频道号 (
COLUMN_DISPLAY_NUMBER
) - 图标(电视输入清单中的
android:icon
) - 节目描述 (
COLUMN_SHORT_DESCRIPTION
) - 节目标题 (
COLUMN_TITLE
) - 频道徽标 (
TvContract.Channels.Logo
)- 使用颜色 #EEEEEE 以匹配周围文本
- 不包含内边距
- 海报艺术作品 (
COLUMN_POSTER_ART_URI
)- 宽高比介于 16:9 和 4:3 之间

图 1. 系统电视应用频道和节目信息呈现器。
系统电视应用通过节目指南提供相同的信息,包括海报艺术作品,如图 2 所示。

图 2. 系统电视应用节目指南。
更新频道数据
更新现有频道数据时,请使用 update()
方法,而不是删除并重新添加数据。选择要更新的记录时,您可以使用 Channels.COLUMN_VERSION_NUMBER
和 Programs.COLUMN_VERSION_NUMBER
来标识数据的当前版本。
注意:将频道数据添加到 ContentProvider
可能需要时间。只有在您配置 EpgSyncJobService
以在后台更新其余频道数据时,才添加当前节目(当前时间两小时内的节目)。请参阅 Android TV 直播电视示例应用以获取示例。
批量加载频道数据
当使用大量频道数据更新系统数据库时,请使用 ContentResolver
的 applyBatch()
或 bulkInsert()
方法。以下是使用 applyBatch()
的示例:
Kotlin
val ops = ArrayList<ContentProviderOperation>() val programsCount = channelInfo.mPrograms.size channelInfo.mPrograms.forEachIndexed { index, program -> ops += ContentProviderOperation.newInsert( TvContract.Programs.CONTENT_URI).run { withValues(programs[index]) withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, programStartSec * 1000) withValue( TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, (programStartSec + program.durationSec) * 1000 ) build() } programStartSec += program.durationSec if (index % 100 == 99 || index == programsCount - 1) { try { contentResolver.applyBatch(TvContract.AUTHORITY, ops) } catch (e: RemoteException) { Log.e(TAG, "Failed to insert programs.", e) return } catch (e: OperationApplicationException) { Log.e(TAG, "Failed to insert programs.", e) return } ops.clear() } }
Java
ArrayList<ContentProviderOperation> ops = new ArrayList<>(); int programsCount = channelInfo.mPrograms.size(); for (int j = 0; j < programsCount; ++j) { ProgramInfo program = channelInfo.mPrograms.get(j); ops.add(ContentProviderOperation.newInsert( TvContract.Programs.CONTENT_URI) .withValues(programs.get(j)) .withValue(Programs.COLUMN_START_TIME_UTC_MILLIS, programStartSec * 1000) .withValue(Programs.COLUMN_END_TIME_UTC_MILLIS, (programStartSec + program.durationSec) * 1000) .build()); programStartSec = programStartSec + program.durationSec; if (j % 100 == 99 || j == programsCount - 1) { try { getContentResolver().applyBatch(TvContract.AUTHORITY, ops); } catch (RemoteException | OperationApplicationException e) { Log.e(TAG, "Failed to insert programs.", e); return; } ops.clear(); } }
异步处理频道数据
数据操作,例如从服务器获取流或访问数据库,不应阻塞界面线程。使用 AsyncTask
是一种异步执行更新的方法。例如,从后端服务器加载频道信息时,您可以按如下方式使用 AsyncTask
:
Kotlin
private class LoadTvInputTask(val context: Context) : AsyncTask<Uri, Unit, Unit>() { override fun doInBackground(vararg uris: Uri) { try { fetchUri(uris[0]) } catch (e: IOException) { Log.d("LoadTvInputTask", "fetchUri error") } } @Throws(IOException::class) private fun fetchUri(videoUri: Uri) { context.contentResolver.openInputStream(videoUri).use { inputStream -> Xml.newPullParser().also { parser -> try { parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) parser.setInput(inputStream, null) sTvInput = ChannelXMLParser.parseTvInput(parser) sSampleChannels = ChannelXMLParser.parseChannelXML(parser) } catch (e: XmlPullParserException) { e.printStackTrace() } } } } }
Java
private static class LoadTvInputTask extends AsyncTask<Uri, Void, Void> { private Context mContext; public LoadTvInputTask(Context context) { mContext = context; } @Override protected Void doInBackground(Uri... uris) { try { fetchUri(uris[0]); } catch (IOException e) { Log.d("LoadTvInputTask", "fetchUri error"); } return null; } private void fetchUri(Uri videoUri) throws IOException { InputStream inputStream = null; try { inputStream = mContext.getContentResolver().openInputStream(videoUri); XmlPullParser parser = Xml.newPullParser(); try { parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(inputStream, null); sTvInput = ChannelXMLParser.parseTvInput(parser); sSampleChannels = ChannelXMLParser.parseChannelXML(parser); } catch (XmlPullParserException e) { e.printStackTrace(); } } finally { if (inputStream != null) { inputStream.close(); } } } }
如果您需要定期更新 EPG 数据,请考虑使用 WorkManager
在空闲时间运行更新过程,例如每天凌晨 3:00。
将数据更新任务与界面线程分离的其他技术包括使用 HandlerThread
类,或者您可以使用 Looper
和 Handler
类自行实现。有关更多信息,请参阅进程和线程。
添加应用链接信息
频道可以使用应用链接,让用户在观看频道内容时轻松启动相关 Activity。频道应用使用应用链接来启动显示相关信息或额外内容的 Activity,从而扩展用户参与度。例如,您可以使用应用链接执行以下操作:
- 引导用户发现和购买相关内容。
- 提供有关当前播放内容的额外信息。
- 在观看系列内容时,开始观看系列中的下一集。
- 让用户与内容互动(例如,评价或评论内容),而不会中断内容播放。
当用户在观看频道内容时按下 选择 以显示电视菜单时,会显示应用链接。

图 1. 在显示频道内容时,频道行上显示的应用链接示例。
当用户选择应用链接时,系统会使用频道应用指定的 intent URI 启动一个 Activity。在应用链接 Activity 处于活动状态时,频道内容会继续播放。用户可以通过按 返回 返回频道内容。
提供应用链接频道数据
Android TV 会自动为每个频道创建应用链接,使用频道数据中的信息。要提供应用链接信息,请在您的 TvContract.Channels
字段中指定以下详细信息:
COLUMN_APP_LINK_COLOR
- 此频道应用链接的强调色。有关强调色示例,请参见图 2 中的标注 3。COLUMN_APP_LINK_ICON_URI
- 此频道应用链接的应用徽章图标的 URI。有关应用徽章图标示例,请参见图 2 中的标注 2。COLUMN_APP_LINK_INTENT_URI
- 此频道应用链接的 intent URI。您可以使用toUri(int)
和URI_INTENT_SCHEME
创建此 URI,并使用parseUri()
将 URI 转换回原始 intent。COLUMN_APP_LINK_POSTER_ART_URI
- 用作此频道应用链接背景的海报艺术作品的 URI。有关海报图像示例,请参见图 2 中的标注 1。COLUMN_APP_LINK_TEXT
- 此频道应用链接的描述性链接文本。有关应用链接描述示例,请参见图 2 中的标注 3 的文本。

图 2. 应用链接详情。
如果频道数据未指定应用链接信息,系统会创建默认应用链接。系统会按如下方式选择默认详细信息:
- 对于 intent URI (
COLUMN_APP_LINK_INTENT_URI
),系统使用CATEGORY_LEANBACK_LAUNCHER
类别的ACTION_MAIN
Activity,通常在应用清单中定义。如果未定义此 Activity,则会出现一个不起作用的应用链接——如果用户点击它,将不会发生任何事情。 - 对于描述性文本 (
COLUMN_APP_LINK_TEXT
),系统使用“打开 app-name”。如果没有定义可用的应用链接 intent URI,系统会使用“无可用链接”。 - 对于强调色 (
COLUMN_APP_LINK_COLOR
),系统使用默认应用颜色。 - 对于海报图像 (
COLUMN_APP_LINK_POSTER_ART_URI
),系统使用应用的主屏幕横幅。如果应用未提供横幅,系统会使用默认电视应用图像。 - 对于徽章图标 (
COLUMN_APP_LINK_ICON_URI
),系统使用显示应用名称的徽章。如果系统也使用应用横幅或默认应用图像作为海报图像,则不显示应用徽章。
您可以在应用设置 Activity 中为您的频道指定应用链接详细信息。您可以随时更新这些应用链接详细信息,因此,如果应用链接需要与频道更改匹配,请更新应用链接详细信息并根据需要调用 ContentResolver.update()
。有关更新频道数据的更多详细信息,请参阅更新频道数据。