开发 TV 输入服务

电视输入服务代表媒体流源,并允许您以线性广播电视的方式将媒体内容呈现为频道和节目。通过 TV 输入服务,您可以提供家长控制、收视指南信息和内容分级。TV 输入服务采用 Android 系统 TV 应用。此应用最终在 TV 上控制并呈现频道内容。系统 TV 应用是专门针对设备开发的,第三方应用无法更改。有关电视输入框架 (TIF) 架构及其组件的更多信息,请参阅 电视输入框架

使用 TIF 随播内容库创建 TV 输入服务

TIF 随播内容库框架提供常见 TV 输入服务功能的可扩展实现。它专供 OEM 为 Android 5.0(API 级别 21)到 Android 7.1(API 级别 25)构建频道。

更新项目

OEM 可在 androidtv-sample-inputs 代码库中找到 TIF 随播内容库,以供旧版使用。如需查看如何在应用中添加该库的示例,请参阅该代码库。

在清单中声明 TV 输入服务

您的应用必须提供与 TvInputService 兼容的服务,供系统用来访问应用。TIF 随播内容库提供 BaseTvInputService 类,该类可默认实现您可以自定义的 TvInputService。创建 BaseTvInputService 的子类,并在清单中将该子类声明为服务。

在清单声明内,指定 BIND_TV_INPUT 权限,允许该服务将 TV 输入连接到系统。系统服务执行绑定并具有 BIND_TV_INPUT 权限。系统电视应用程序通过TvInputManager接口向电视输入服务发送请求。

在服务声明中,请添加一个 intent 过滤器,将 TvInputService 指定为通过该 intent 执行的操作。另外,请将服务元数据声明为单独的 XML 资源。以下示例演示了服务声明、intent 过滤器和服务元数据声明:

<service android:name=".rich.RichTvInputService"
    android:label="@string/rich_input_label"
    android:permission="android.permission.BIND_TV_INPUT">
    <!-- Required filter used by the system to launch our account service. -->
    <intent-filter>
        <action android:name="android.media.tv.TvInputService" />
    </intent-filter>
    <!-- An XML file which describes this input. This provides pointers to
    the RichTvInputSetupActivity to the system/TV app. -->
    <meta-data
        android:name="android.media.tv.input"
        android:resource="@xml/richtvinputservice" />
</service>

在单独的 XML 文件中定义服务元数据。服务元数据 XML 文件必须包含描述 TV 输入的初始配置和频道扫描的设置界面。元数据文件还应包含一个说明用户是否能够录制内容的标记。如需详细了解如何在应用中支持录制内容,请参阅支持内容录制

服务元数据文件位于您的应用的 XML 资源目录中,并且必须与您在清单中声明的资源名称相匹配。利用上例中的清单条目,您可以在 res/xml/richtvinputservice.xml 创建含有以下内容的 XML 文件:

<?xml version="1.0" encoding="utf-8"?>
<tv-input xmlns:android="http://schemas.android.com/apk/res/android"
  android:canRecord="true"
  android:setupActivity="com.example.android.sampletvinput.rich.RichTvInputSetupActivity" />

定义频道并创建设置 Activity

您的电视输入服务必须至少定义一个用户可通过系统电视应用访问的频道。您应该在系统数据库中注册您的频道,并提供一个设置活动,当系统找不到您的应用所需的频道时,系统将调用该活动。

首先,启用您的应用程序,使其能够读取和写入系统电子节目指南 (EPG),其数据包括用户可访问的频道和节目。如需启用您的应用以执行这些操作,并在设备重启后与 EPG 同步,请将以下元素添加到您的应用清单:

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

添加以下元素,以确保您的应用在 Google Play 商店中显示为提供 Android TV 内容频道的应用:

<uses-feature
    android:name="android.software.live_tv"
    android:required="true" />

接下来,创建一个扩展 EpgSyncJobService 类的类。通过此抽象类,您可以创建能够在系统数据库中创建并更新频道的作业服务。

在你的子类中,创建并返回 getChannels 中的完整通道列表。如果您的频道来自 XMLTV 文件,请使用 XmlTvParser 类。否则,使用 Channel.Builder 类以编程方式生成频道。

对于每个频道,当系统需要在给定时间段内可在相应频道上观看的节目列表时,系统会调用 getProgramsForChannel。针对该频道返回 Program 对象列表。使用 XmlTvParser 类从 XMLTV 文件获取节目,或使用 Program.Builder 类以编程方式生成节目。

对于每个 Program 对象,请使用 InternalProviderData 对象设置节目信息,如节目的视频类型。如果您只想让频道循环播放有限数量的节目,请在设置节目信息时使用 InternalProviderData.setRepeatable 方法,并将值为 true

实现该作业服务后,将其添加到您的应用清单中:

<service
    android:name=".sync.SampleJobService"
    android:permission="android.permission.BIND_JOB_SERVICE"
    android:exported="true" />

最后,创建设置 Activity。您的设置 activity 应该提供一种同步频道和节目数据的方式。其中一种方式是让用户通过界面在 activity 中操作。也可在 activity 启动时,让应用自动操作。当设置 activity 需要同步频道和节目信息时,应用应启动此作业服务:

Kotlin

val inputId = getActivity().intent.getStringExtra(TvInputInfo.EXTRA_INPUT_ID)
EpgSyncJobService.cancelAllSyncRequests(getActivity())
EpgSyncJobService.requestImmediateSync(
        getActivity(),
        inputId,
        ComponentName(getActivity(), SampleJobService::class.java)
)

Java

String inputId = getActivity().getIntent().getStringExtra(TvInputInfo.EXTRA_INPUT_ID);
EpgSyncJobService.cancelAllSyncRequests(getActivity());
EpgSyncJobService.requestImmediateSync(getActivity(), inputId,
        new ComponentName(getActivity(), SampleJobService.class));

使用 requestImmediateSync 方法同步作业服务。用户必须等待同步完成,因此您应该保持相对较短的请求期。

使用 setUpPeriodicSync 方法让作业服务在后台定期同步频道和节目数据:

Kotlin

EpgSyncJobService.setUpPeriodicSync(
        context,
        inputId,
        ComponentName(context, SampleJobService::class.java)
)

Java

EpgSyncJobService.setUpPeriodicSync(context, inputId,
        new ComponentName(context, SampleJobService.class));

TIF 随播内容库提供 requestImmediateSync 的额外过载方法,让您能够指定要同步的频道数据的持续时间(以毫秒为单位)。默认方法会同步一小时的频道数据。

TIF 随播内容库还提供 setUpPeriodicSync 的额外过载方法,让您能够指定要同步的频道数据的持续时间,以及应发生定期同步的频率。默认方法每隔 12 小时同步 48 小时的频道数据。

如需详细了解频道数据和电子节目单,请参阅 使用频道数据

处理微调请求和媒体播放

当用户选择特定频道时,系统 TV 应用使用您的应用创建的 Session,微调到请求的频道并播放内容。TIF 随播内容库提供多个类,您可以扩展这些类来处理频道和系统中的会话调用。

您的 BaseTvInputService 子类创建的会话可处理微调请求。替换 onCreateSession 方法,创建从 BaseTvInputService.Session 类扩展的会话,并使用新会话调用 super.sessionCreated。在以下示例中,onCreateSession 返回 RichTvInputSessionImpl 对象,该对象扩展 BaseTvInputService.Session

Kotlin

override fun onCreateSession(inputId: String): Session =
        RichTvInputSessionImpl(this, inputId).apply {
            setOverlayViewEnabled(true)
        }

Java

@Override
public final Session onCreateSession(String inputId) {
    RichTvInputSessionImpl session = new RichTvInputSessionImpl(this, inputId);
    session.setOverlayViewEnabled(true);
    return session;
}

当用户使用系统 TV 应用开始观看其中一个频道时,系统会调用您会话的 onPlayChannel 方法。如果在程序开始播放之前需要进行任何特殊的频道初始化,请重写此方法。

然后,系统获取当前安排的节目并调用您会话的 onPlayProgram 方法,指定节目信息和开始时间(以毫秒为单位)。使用 TvPlayer 界面开始播放程序。

您的媒体播放器代码应实现 TvPlayer 来处理特定的播放事件。TvPlayer 类处理时移控制等功能,而不会增加 BaseTvInputService 实现的复杂性。

在会话的 getTvPlayer 方法中,返回实现 TvPlayer 的媒体播放器。 电视输入服务 示例应用程序实现了一个使用 ExoPlayer 的媒体播放器。

使用 TV 输入框架创建 TV 输入服务

如果您的电视输入服务无法使用 TIF 配套库,则需要实现以下组件:

您还需要执行以下操作:

  1. 在清单中声明您的 TV 输入服务,如在清单中声明您的 TV 输入服务中所述。
  2. 创建服务元数据文件。
  3. 创建并注册您的频道和节目信息。
  4. 创建您的设置 Activity。

定义您的 TV 输入服务

对于您的服务,您可以扩展 TvInputService 类。TvInputService 实现是一个绑定服务,其中系统服务是与其绑定的客户端。您需要实现的服务生命周期方法如图 1 所示。

onCreate 方法初始化并启动 HandlerThread,后者提供与界面线程分开的进程线程以处理系统驱动的操作。在下例中,onCreate 方法初始化 CaptioningManager 并准备处理 ACTION_BLOCKED_RATINGS_CHANGEDACTION_PARENTAL_CONTROLS_ENABLED_CHANGED 操作。这些操作描述了当用户更改家长控制设置时,以及当已屏蔽分级列表上发生更改时触发的系统 intent。

Kotlin

override fun onCreate() {
    super.onCreate()
    handlerThread = HandlerThread(javaClass.simpleName).apply {
        start()
    }
    dbHandler = Handler(handlerThread.looper)
    handler = Handler()
    captioningManager = getSystemService(Context.CAPTIONING_SERVICE) as CaptioningManager

    setTheme(android.R.style.Theme_Holo_Light_NoActionBar)

    sessions = mutableListOf<BaseTvInputSessionImpl>()
    val intentFilter = IntentFilter().apply {
        addAction(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED)
        addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED)
    }
    registerReceiver(broadcastReceiver, intentFilter)
}

Java

@Override
public void onCreate() {
    super.onCreate();
    handlerThread = new HandlerThread(getClass()
      .getSimpleName());
    handlerThread.start();
    dbHandler = new Handler(handlerThread.getLooper());
    handler = new Handler();
    captioningManager = (CaptioningManager)
      getSystemService(Context.CAPTIONING_SERVICE);

    setTheme(android.R.style.Theme_Holo_Light_NoActionBar);

    sessions = new ArrayList<BaseTvInputSessionImpl>();
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(TvInputManager
      .ACTION_BLOCKED_RATINGS_CHANGED);
    intentFilter.addAction(TvInputManager
      .ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
    registerReceiver(broadcastReceiver, intentFilter);
}

图 1. TvInputService 生命周期。

有关处理被屏蔽内容和提供家长控制的更多信息,请参阅 控制内容。如需详细了解建议在 TV 输入服务中处理的系统驱动的操作,请参阅 TvInputManager

TvInputService 会创建可实现 Handler.CallbackTvInputService.Session,以处理播放器状态更改。借助 onSetSurfaceTvInputService.Session 会使用视频内容设置 Surface。有关使用 Surface 渲染视频的更多信息,请参阅 将播放器与 surface

TvInputService.Session 处理用户选择频道时的 onTune 事件,并通知系统电视应用程序内容和内容元数据的更改。在本培训中的 控制内容处理轨道选择中,对这些 notify 方法做了进一步的说明。

定义您的设置 Activity

系统 TV 应用使用您为 TV 输入定义的设置 Activity。设置 activity 是必需的,并且必须为系统数据库至少提供一个频道记录。当系统电视应用程序找不到电视输入频道时,它会调用设置活动。

设置 activity 向系统 TV 应用描述通过 TV 输入提供的频道,如下一课创建并更新频道数据中所示。

其他参考资料