如何实现一个自定义的 TV Input Service?
解读
在国内面试中,这道题考察的是候选人是否真正接触过 Android TV 系统级开发,而不仅仅是“会写 App”。TV Input Framework(TIF)是 Android 官方留给运营商、盒子厂商、内容牌照方的“后门”,通过实现一个 TvInputService 可以把自家直播流、点播数据、EPG 信息无缝塞进系统“直播频道”里,与系统 TV 应用(Live TV、Google TV、国内厂商定制桌面)零耦合集成。面试官想确认:
- 你是否知道 TIF 的整体角色、权限模型和进程边界;
- 能否把“信号源→会话→视频输出”这条链路打通;
- 是否理解国内“无 GMS、有内容安全”场景下的适配点,如自签证书、DRM、码流格式、后台保活、备案审核等。
答不到“系统权限”“surface 传递”“TvView 合成”这三个关键词,基本会被判定为“只写过普通视频播放器”。
知识点
-
TvInputFramework 组成
- TvInputService:厂商实现,运行在:tv 独立进程,系统 bindService 拉起。
- TvInputInfo:xml 声明,描述输入源类型(TYPE_TUNER/TYPE_HDMI/TYPE_OTHER)、图标、标签、隐藏/推荐位。
- Session:每换台创建,负责向系统提供音轨、字幕、视频 Surface、DRM 会话、Epg 数据。
- TvView/TvInputManager:系统 TV App 侧控件,通过 IPC 把 Surface 下到 Session,完成合成。
-
权限与沙箱
- 系统级签名:国内盒子普遍走 AOSP,需在 mk 里把 service 声明到 priv-app,用平台 key 签名,否则 TvInputManager 拒绝注册。
- android.permission.BIND_TV_INPUT、READ_TV_LIST、ACCESS_TUNER_INFO 等“signature|privileged”权限必须预授权。
-
视频输出链路
- Session.onSetSurface(Surface) 由系统 TvView 把硬件层(Overlay)或 GPU 纹理传下来,厂商 MediaCodec/ExoPlayer 直接 setOutputSurface。
- 若走 Tuner 或 HDMI 直通,可返回 VIDEO_UNAVAILABLE_REASON_UNKNOWN,让系统切换到底层 HAL。
-
EPG 与内容评级
- 通过 TvContract.Programs、TvContract.Channels ContentProvider 写入节目单;国内审核要求过滤暴力/政治敏感,需在程序里做二次分级(ContentRatingSystem)。
-
国内适配细节
- 无 GMS 场景:去掉 TIF 对 TvProvider 的 GMS 限制补丁,自行实现 TvProvider.apk;
- 直播源多为组播 UDP,需加本地代理守护进程,防止系统杀后台;
- 投屏/广告插播:在 Session.onTimeShiftPlay 里插入本地 Server 实现“时移广告”,通过 ContentRecordingClient 录制 30 s 再回放。
-
性能与调试
- 使用 adb shell cmd tv_input 调试:dump、channel-browse、art 模式;
- 帧率对齐:Surface 必须 50 Hz/60 Hz 与显示输出一致,否则系统 TvView 会报 Jank;
- 内存:TvInputService 常驻,binder 泄漏会直接导致系统 TV 闪退,需用 Perfetto 追踪 binder ref。
答案
下面给出一条“最小可运行”且符合国内盒子无 GMS 场景的完整思路,面试时按“步骤+关键代码+权限”口头描述即可,无需写满黑板。
-
建立系统级模块
在 AOSP 源码树 packages/apps/ 新建 MyTvInput,Android.mk 中声明:- LOCAL_PRIVILEGED_MODULE := true
- LOCAL_CERTIFICATE := platform
保证生成的 apk 在 /system/priv-app/,拥有系统签名。
-
Manifest 注册
<service android:name=".MyTvInputService" android:permission="android.permission.BIND_TV_INPUT">
<intent-filter>
<action android:name="android.media.tv.TvInputService" />
</intent-filter>
<meta-data android:name="android.media.tv.input" android:resource="@xml/my_tv_input" />
</service>xml/my_tv_input.xml
<tv-input xmlns:android="http://schemas.android.com/apk/res/android" android:canRecord="true" android:setupActivity="com.xxx.SetupActivity" /> -
实现 TvInputService
class MyTvInputService : TvInputService() {
override fun onCreateSession(inputId: String): Session = MySession(this, inputId)
}inner class MySession(context: Context, inputId: String) : Session(context, inputId) {
private var player: ExoPlayer
private var currentSurface: Surface? = nulloverride fun onSetSurface(surface: Surface?) { currentSurface = surface player.setVideoSurface(surface) } override fun onTune(uri: Uri): Boolean { // uri 来自系统 TvView,形如 content://android.media.tv/channel/101 val channel = TvContract.buildChannelUri(uri) val url = queryChannelUrl(channel) // 本地查表拿到 UDP 组播地址 player.setMediaItem(MediaItem.fromUri(url)) player.prepare() notifyVideoAvailable() return true } override fun onSetStreamVolume(volume: Float) { player.volume = volume }}
-
写入 EPG
在 SetupActivity 里首次启动时把频道、节目单批量写进 TvProvider:
val ops = ArrayList<ContentProviderOperation>()
for (c in channelList) {
ops.add(ContentProviderOperation.newInsert(TvContract.Channels.CONTENT_URI)
.withValue(TvContract.Channels.COLUMN_INPUT_ID, inputId)
.withValue(TvContract.Channels.COLUMN_DISPLAY_NAME, c.name)
.withValue(TvContract.Channels.COLUMN_TYPE, TvContract.Channels.TYPE_OTHER)
.build())
}
contentResolver.applyBatch(TvContract.AUTHORITY, ops) -
权限与 SELinux
在 sepolicy 给 priv_app 加:
allow priv_app tv_input_service:service_manager find;
allow priv_app tvprovider_db:file { read write };
否则系统拒绝 bindService 或写库。 -
调试验证
adb shell cmd tv_input list
adb shell am start -n com.android.tv/.MainActivity
观察 logcat 打印 TvInputManagerService: MyTvInputService connected.
切台无报错、音画同步即算打通。
拓展思考
-
多实例与 HDMI-CEC
国内运营商盒子常把“OTT 直播”与“HDMI 地面波”做在同一 APK 里,需要为两个 inputId 分别建 Session,并在 TvInputInfo 里声明 parentId,实现一键无缝换源;同时实现 HdmiControlService 处理遥控器一键开机联动。 -
时移与云端录制
利用 TvInputService.onTimeShiftPause() 把直播流边录边推到私有云,回放时返回 https 点播地址;注意广电总局要求录制必须加水印、3 天后删除,需在服务端做切片转码与 DRM 二次加密。 -
广告动态替换
在 Session 侧监听 SCTE-35 信号,识别广告插入点,暂停本地 UDP 组播,把广告片 URL 塞进 ExoPlayer 的 ConcatenatingMediaSource,实现“台网联动”广告,提升 ARPU;同时上报 AdEvent 到系统 TvProvider,方便运营后台统计。 -
折叠屏/车载扩展
Android 14 开始支持桌面多窗格,TvView 可以嵌入到折叠屏副屏或车载后座娱乐;此时需处理 onSetSurface 多次回调,动态切换分辨率(1080p/720p),并兼容车载音频焦点策略(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)。 -
安全合规
国内上线前需通过“互联网电视内容牌照方”集成审核,重点检查 TvInputService 是否可绕过家长控制、是否可后台采集用户收视行为;务必在代码里显式调用 ContentRatingSystem.isRatingBlocked(),并加密存储用户日志,否则会被下架并计入广电总局黑名单。