如何实现一个自定义的 TV Input Service?

解读

在国内面试中,这道题考察的是候选人是否真正接触过 Android TV 系统级开发,而不仅仅是“会写 App”。TV Input Framework(TIF)是 Android 官方留给运营商、盒子厂商、内容牌照方的“后门”,通过实现一个 TvInputService 可以把自家直播流、点播数据、EPG 信息无缝塞进系统“直播频道”里,与系统 TV 应用(Live TV、Google TV、国内厂商定制桌面)零耦合集成。面试官想确认:

  1. 你是否知道 TIF 的整体角色、权限模型和进程边界;
  2. 能否把“信号源→会话→视频输出”这条链路打通;
  3. 是否理解国内“无 GMS、有内容安全”场景下的适配点,如自签证书、DRM、码流格式、后台保活、备案审核等。

答不到“系统权限”“surface 传递”“TvView 合成”这三个关键词,基本会被判定为“只写过普通视频播放器”。

知识点

  1. TvInputFramework 组成

    • TvInputService:厂商实现,运行在:tv 独立进程,系统 bindService 拉起。
    • TvInputInfo:xml 声明,描述输入源类型(TYPE_TUNER/TYPE_HDMI/TYPE_OTHER)、图标、标签、隐藏/推荐位。
    • Session:每换台创建,负责向系统提供音轨、字幕、视频 Surface、DRM 会话、Epg 数据。
    • TvView/TvInputManager:系统 TV App 侧控件,通过 IPC 把 Surface 下到 Session,完成合成。
  2. 权限与沙箱

    • 系统级签名:国内盒子普遍走 AOSP,需在 mk 里把 service 声明到 priv-app,用平台 key 签名,否则 TvInputManager 拒绝注册。
    • android.permission.BIND_TV_INPUT、READ_TV_LIST、ACCESS_TUNER_INFO 等“signature|privileged”权限必须预授权。
  3. 视频输出链路

    • Session.onSetSurface(Surface) 由系统 TvView 把硬件层(Overlay)或 GPU 纹理传下来,厂商 MediaCodec/ExoPlayer 直接 setOutputSurface。
    • 若走 Tuner 或 HDMI 直通,可返回 VIDEO_UNAVAILABLE_REASON_UNKNOWN,让系统切换到底层 HAL。
  4. EPG 与内容评级

    • 通过 TvContract.Programs、TvContract.Channels ContentProvider 写入节目单;国内审核要求过滤暴力/政治敏感,需在程序里做二次分级(ContentRatingSystem)。
  5. 国内适配细节

    • 无 GMS 场景:去掉 TIF 对 TvProvider 的 GMS 限制补丁,自行实现 TvProvider.apk;
    • 直播源多为组播 UDP,需加本地代理守护进程,防止系统杀后台;
    • 投屏/广告插播:在 Session.onTimeShiftPlay 里插入本地 Server 实现“时移广告”,通过 ContentRecordingClient 录制 30 s 再回放。
  6. 性能与调试

    • 使用 adb shell cmd tv_input 调试:dump、channel-browse、art 模式;
    • 帧率对齐:Surface 必须 50 Hz/60 Hz 与显示输出一致,否则系统 TvView 会报 Jank;
    • 内存:TvInputService 常驻,binder 泄漏会直接导致系统 TV 闪退,需用 Perfetto 追踪 binder ref。

答案

下面给出一条“最小可运行”且符合国内盒子无 GMS 场景的完整思路,面试时按“步骤+关键代码+权限”口头描述即可,无需写满黑板。

  1. 建立系统级模块
    在 AOSP 源码树 packages/apps/ 新建 MyTvInput,Android.mk 中声明:

    • LOCAL_PRIVILEGED_MODULE := true
    • LOCAL_CERTIFICATE := platform
      保证生成的 apk 在 /system/priv-app/,拥有系统签名。
  2. 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" />

  3. 实现 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? = null

    override 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 }  
    

    }

  4. 写入 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)

  5. 权限与 SELinux
    在 sepolicy 给 priv_app 加:
    allow priv_app tv_input_service:service_manager find;
    allow priv_app tvprovider_db:file { read write };
    否则系统拒绝 bindService 或写库。

  6. 调试验证
    adb shell cmd tv_input list
    adb shell am start -n com.android.tv/.MainActivity
    观察 logcat 打印 TvInputManagerService: MyTvInputService connected.
    切台无报错、音画同步即算打通。

拓展思考

  1. 多实例与 HDMI-CEC
    国内运营商盒子常把“OTT 直播”与“HDMI 地面波”做在同一 APK 里,需要为两个 inputId 分别建 Session,并在 TvInputInfo 里声明 parentId,实现一键无缝换源;同时实现 HdmiControlService 处理遥控器一键开机联动。

  2. 时移与云端录制
    利用 TvInputService.onTimeShiftPause() 把直播流边录边推到私有云,回放时返回 https 点播地址;注意广电总局要求录制必须加水印、3 天后删除,需在服务端做切片转码与 DRM 二次加密。

  3. 广告动态替换
    在 Session 侧监听 SCTE-35 信号,识别广告插入点,暂停本地 UDP 组播,把广告片 URL 塞进 ExoPlayer 的 ConcatenatingMediaSource,实现“台网联动”广告,提升 ARPU;同时上报 AdEvent 到系统 TvProvider,方便运营后台统计。

  4. 折叠屏/车载扩展
    Android 14 开始支持桌面多窗格,TvView 可以嵌入到折叠屏副屏或车载后座娱乐;此时需处理 onSetSurface 多次回调,动态切换分辨率(1080p/720p),并兼容车载音频焦点策略(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)。

  5. 安全合规
    国内上线前需通过“互联网电视内容牌照方”集成审核,重点检查 TvInputService 是否可绕过家长控制、是否可后台采集用户收视行为;务必在代码里显式调用 ContentRatingSystem.isRatingBlocked(),并加密存储用户日志,否则会被下架并计入广电总局黑名单。