使用频道数据

您的电视输入必须在设置活动中至少为一个频道提供电子节目指南 (EPG) 数据。您还应定期更新这些数据,并考虑更新的大小以及处理更新的线程。此外,您可以为频道提供应用链接,引导用户访问相关内容和活动。本课程将讨论在系统数据库中创建和更新频道和节目数据,并考虑到这些因素。

试用 TV 输入服务 示例应用。

获取权限

为了使您的电视输入能够使用 EPG 数据,它必须在 Android 清单文件中声明写入权限,如下所示

<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />

在数据库中注册频道

Android TV 系统数据库维护 TV 输入的频道数据记录。在设置活动中,对于您的每个频道,您必须将您的频道数据映射到 TvContract.Channels 类的以下字段

虽然 TV 输入框架足够通用,可以处理传统广播和点播 (OTT) 内容而没有任何区别,但您可能希望除了上述列之外还定义以下列,以便更好地识别传统广播频道

如果您想为您的频道提供应用链接详细信息,则需要更新一些其他字段。有关应用链接字段的更多信息,请参阅 添加应用链接信息.

对于基于互联网流的 TV 输入,请相应地为您自己的值分配上述值,以便每个频道都可以被唯一地识别。

从您的后端服务器拉取您的频道元数据(以 XML、JSON 或任何其他格式),并在您的设置活动中将值映射到系统数据库,如下所示

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 所示。为确保频道和节目信息与系统电视应用程序的频道和节目信息呈现器一起使用,请遵循以下准则。

  1. 频道号 (COLUMN_DISPLAY_NUMBER)
  2. 图标 (android:icon 在电视输入的清单中)
  3. 节目描述 (COLUMN_SHORT_DESCRIPTION)
  4. 节目标题 (COLUMN_TITLE)
  5. 频道标志 (TvContract.Channels.Logo)
    • 使用颜色 #EEEEEE 与周围文本匹配
    • 不要包含填充
  6. 海报艺术 (COLUMN_POSTER_ART_URI)
    • 长宽比在 16:9 和 4:3 之间

图 1. 系统电视应用程序频道和节目信息呈现器。

系统电视应用程序通过节目指南提供相同的信息,包括海报艺术,如图 2 所示。

图 2. 系统电视应用程序节目指南。

更新频道数据

更新现有频道数据时,请使用 update() 方法,而不是删除和重新添加数据。您可以使用 Channels.COLUMN_VERSION_NUMBERPrograms.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();
    }
}

异步处理频道数据

数据操作(例如从服务器获取流或访问数据库)不应阻塞 UI 线程。使用 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)运行更新过程。

将数据更新任务与 UI 线程分离的其他技术包括使用 HandlerThread 类,或者可以使用 LooperHandler 类实现自己的技术。有关更多信息,请参阅 进程和线程

频道可以使用应用程序链接,让用户在观看频道内容时轻松启动相关活动。频道应用程序使用应用程序链接通过启动显示相关信息或附加内容的活动来扩展用户参与度。例如,您可以使用应用程序链接执行以下操作

  • 引导用户发现和购买相关内容。
  • 提供有关当前播放内容的附加信息。
  • 在观看剧集内容时,开始观看剧集的下一集。
  • 让用户与内容互动(例如,评价或评论内容),而不会中断内容播放。

当用户按选择显示电视菜单时,会显示应用程序链接,同时观看频道内容。

图 1. 频道内容显示时,在频道行上显示的应用程序链接示例。

当用户选择应用程序链接时,系统会使用频道应用程序指定的意图 URI 启动一个活动。频道内容会在应用程序链接活动处于活动状态时继续播放。用户可以通过按返回返回到频道内容。

提供应用程序链接频道数据

Android TV 会自动为每个频道创建应用程序链接,使用频道数据中的信息。要提供应用程序链接信息,请在您的 TvContract.Channels 字段中指定以下详细信息

  • COLUMN_APP_LINK_COLOR - 此频道应用程序链接的强调色。有关强调色的示例,请参阅图 2,标注 3。
  • COLUMN_APP_LINK_ICON_URI - 此频道应用程序链接的应用程序徽章图标的 URI。有关应用程序徽章图标的示例,请参阅图 2,标注 2。
  • COLUMN_APP_LINK_INTENT_URI - 此频道应用程序链接的意图 URI。您可以使用 toUri(int)(使用 URI_INTENT_SCHEME)创建 URI,并使用 parseUri() 将 URI 转换回原始意图。
  • COLUMN_APP_LINK_POSTER_ART_URI - 用作此频道应用程序链接背景的海报艺术的 URI。有关海报图像的示例,请参阅图 2,标注 1。
  • COLUMN_APP_LINK_TEXT - 此频道应用程序链接的描述性链接文本。有关应用程序链接描述的示例,请参阅图 2 中标注 3 的文本。

图 2. 应用程序链接详细信息。

如果频道数据未指定应用程序链接信息,系统会创建一个默认应用程序链接。系统会按如下方式选择默认详细信息

  • 对于意图 URI (COLUMN_APP_LINK_INTENT_URI),系统会使用 ACTION_MAIN 活动,用于 CATEGORY_LEANBACK_LAUNCHER 类别,通常在应用程序清单中定义。如果未定义此活动,则会出现一个不可用的应用程序链接——如果用户单击它,则不会发生任何事情。
  • 对于描述性文本 (COLUMN_APP_LINK_TEXT),系统会使用“打开 应用程序名称”。如果未定义可行的应用程序链接意图 URI,系统会使用“无可用链接”。
  • 对于强调色 (COLUMN_APP_LINK_COLOR),系统会使用默认应用程序颜色。
  • 对于海报图像 (COLUMN_APP_LINK_POSTER_ART_URI),系统会使用应用程序的主屏幕横幅。如果应用程序未提供横幅,系统会使用默认电视应用程序图像。
  • 对于徽章图标 (COLUMN_APP_LINK_ICON_URI),系统会使用显示应用程序名称的徽章。如果系统还使用应用程序横幅或默认应用程序图像作为海报图像,则不会显示应用程序徽章。

您在应用程序的设置活动中为频道指定应用程序链接详细信息。您可以随时更新这些应用程序链接详细信息,因此,如果应用程序链接需要与频道更改匹配,请更新应用程序链接详细信息并根据需要调用 ContentResolver.update()。有关更新频道数据的更多详细信息,请参阅 更新频道数据