你的电视输入设备必须至少提供电子收视指南 (EPG) 数据 设置 activity 中的一个渠道。您还应定期更新 数据,需考虑更新的大小和处理线程 资源。此外,您还可以提供频道的应用链接 ,用于引导用户找到相关内容和 activity。 本课介绍如何在 系统数据库。
试用 TV 输入服务示例应用。
获取权限
为了让您的 TV 输入能与电子节目单数据一起使用,它必须声明 写入权限,如下所示:
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
在数据库中注册频道
Android TV 系统数据库维护 TV 输入的频道数据记录。在您的设置中
则您必须将频道数据映射到
TvContract.Channels
类:
COLUMN_DISPLAY_NAME
- 频道COLUMN_DISPLAY_NUMBER
- 显示的频道 号码COLUMN_INPUT_ID
- TV 输入服务的 IDCOLUMN_SERVICE_TYPE
- 频道的服务类型COLUMN_TYPE
- 频道的广播标准 类型COLUMN_VIDEO_FORMAT
- 默认视频格式 频道
尽管 TV 输入框架足够通用,可以同时处理传统广播和 OTT 服务内容,您可能需要在 以更好地识别传统广播频道:
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
是一个对象,用于保存
后端服务器
呈现频道和节目信息
系统 TV 应用会在用户浏览频道时向其呈现频道和节目信息, 如图 1 所示。为了确保频道和节目信息与系统 TV 应用的 频道和节目信息展示者,请遵循以下准则。
- 频道号 (
COLUMN_DISPLAY_NUMBER
) - 图标
(
android:icon
TV 输入源的清单) - 节目说明 (
COLUMN_SHORT_DESCRIPTION
) - 节目名称 (
COLUMN_TITLE
) - 频道徽标 (
TvContract.Channels.Logo
)- 使用颜色 #EEEEEE,让徽标与周围的文字相配
- 不含内边距
- 海报图片 (
COLUMN_POSTER_ART_URI
)- 宽高比介于 16:9 到 4:3 之间
![](https://developer.android.google.cn/static/images/tv/channel-info.png?authuser=9&hl=fi)
图 1. 系统 TV 应用频道和节目信息 Presenter。
系统 TV 应用会通过收视指南提供相同的信息,包括海报图片、 如图 2 所示。
![](https://developer.android.google.cn/static/images/tv/prog-guide.png?authuser=9&hl=fi)
图 2. 系统 TV 应用的收视指南。
更新频道数据
更新现有频道数据时,请使用
update()
方法,而不是删除并重新添加数据。您可以确定数据的当前版本
使用 Channels.COLUMN_VERSION_NUMBER
和Programs.COLUMN_VERSION_NUMBER
选择要更新的记录。
注意:向 ContentProvider
添加频道数据
可能需要一些时间。添加当前节目(距离当前时间不超过两小时的节目)
仅在您配置 EpgSyncJobService
以更新其余配置时
频道数据的相关情况请参阅
<ph type="x-smartling-placeholder"></ph>
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(); } } } }
如果您需要定期更新电子节目单数据,不妨考虑使用
WorkManager
在空闲时间(如每天凌晨 3:00)运行更新流程。
将数据更新任务与界面线程分开的其他方法包括使用
HandlerThread
类,也可以使用 Looper
实现自己的类。
和 Handler
类。请参阅
进程和线程。
添加应用链接信息
频道可以使用应用链接,让用户轻松启动相关的应用 观看频道内容期间的活动频道应用使用 应用链接,通过启动显示以下内容的活动来提升用户互动度: 相关信息或其他内容。例如,您可以使用应用链接 执行以下操作:
- 引导用户发现和购买相关内容。
- 提供有关当前播放内容的其他信息。
- 在观看剧集内容时,开始观看 系列视频
- 允许用户与内容互动,例如评分或评价 不中断内容播放。
当用户按选择以显示 观看频道内容时看到的电视菜单。
![](https://developer.android.google.cn/static/images/training/tv/tif/app-link.png?authuser=9&hl=fi)
图 1. 示例应用链接 显示在频道行中。
当用户选择应用链接时,系统会使用 频道应用指定的 intent URI。频道内容继续播放 当应用关联 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 转换回原始 intentparseUri()
。COLUMN_APP_LINK_POSTER_ART_URI
- 用作应用链接背景的海报图片的 URI 。如需查看海报图片示例,请见图 2 中的标注 1。COLUMN_APP_LINK_TEXT
- 此频道的应用链接的描述性链接文字。示例 应用链接说明,请参见图 2 标注 3 中的文字。
![](https://developer.android.google.cn/static/images/training/tv/tif/app-link-diagram.png?authuser=9&hl=fi)
图 2. 应用链接详细信息。
如果频道数据未指定应用链接信息,系统将 创建默认应用链接。系统会选择默认详细信息,具体说明如下:
- 对于 intent URI
(
COLUMN_APP_LINK_INTENT_URI
), 系统使用ACTION_MAIN
CATEGORY_LEANBACK_LAUNCHER
类别的 activity,通常在应用清单中定义。 如果未定义此 activity,系统会显示一个不起作用的应用链接 - 如果 用户点击它后,什么都不会发生 - 对于说明性文字
(
COLUMN_APP_LINK_TEXT
),系统 使用“打开app-name”。如果未定义可行的应用链接 intent URI, 系统会使用“无可用链接”。 - 对于强调色
(
COLUMN_APP_LINK_COLOR
), 系统使用默认应用颜色。 - 对于海报图片
(
COLUMN_APP_LINK_POSTER_ART_URI
), 系统使用应用的主屏幕横幅。如果应用未提供 横幅广告,系统会使用默认 TV 应用图片。 - 对于徽章图标
(
COLUMN_APP_LINK_ICON_URI
)、 系统使用一个显示应用名称的标志。如果系统也使用 应用横幅或海报图片的默认应用图片,未显示应用标志。
您可以在应用的
设置 activity。你可以随时更新这些应用链接详细信息
如果应用链接需要与频道变更保持一致,请更新应用
链接详情和通话
ContentResolver.update()
(如果需要)。如需详细了解如何更新
请参阅更新频道数据。