第28章 TV与机顶盒场景:TV Input Framework、HDMI CEC、环绕声直通

做Android多媒体开发这么多年,TV和机顶盒这块儿,说实话,是坑最多、也是最考验系统集成能力的地方。你想想看,一个电视盒子要对接多少外设?遥控器、HDMI切换器、功放、音箱……每个设备都有自己的脾气。

这一章,我重点聊聊三个核心模块:TV Input Framework(TIF)、HDMI CEC、以及环绕声直通。这三块搞定了,你的TV产品才算真正能用、好用。

28.1 TV Input Framework:电视输入的“调度中心”

TIF是什么?说白了,它就是Android系统里专门管理电视信号输入的框架。无论是HDMI、AV、DTV(数字电视),还是IP流媒体,都通过TIF统一管理。

我记得第一次接触TIF时,觉得它跟Camera框架有点像——都有Input、Session、Client的概念。但TIF更强调“频道”和“节目”的管理。

28.1.1 TIF的核心组件

TIF主要由三部分组成:

  • TvInputManager:系统服务,负责管理所有TV输入
  • TvInputService:每个硬件输入对应的Service实现
  • TvView:显示TV画面的View组件

来看一个典型的TvInputService实现骨架:

public class HdmiInputService extends TvInputService {
    private HdmiInputSession mSession;

    @Override
    public Session onCreateSession(String inputId) {
        mSession = new HdmiInputSession(this);
        return mSession;
    }

    private class HdmiInputSession extends TvInputService.Session {
        @Override
        public boolean onSetSurface(Surface surface) {
            // 把HDMI解码后的画面渲染到Surface上
            return true;
        }

        @Override
        public boolean onTune(Uri channelUri) {
            // 切换到指定频道
            int channelId = parseChannelId(channelUri);
            return tuneToChannel(channelId);
        }

        @Override
        public void onRelease() {
            // 释放资源
        }
    }
}
我的经验:实现onSetSurface时,一定要处理好Surface的格式。我遇到过HDMI输入要求RGBA,但系统默认给了NV12,结果画面花屏。建议在onSetSurface里主动查询SurfaceHolder的格式要求。

28.1.2 频道扫描与节目信息

TIF里有个很重要的概念叫Channel。每个Channel对应一个节目源,比如“CCTV-1”对应一个DTV频道,“HDMI 1”对应一个外部输入。

频道扫描的逻辑通常放在TvInputService的onCreateOverlayView里。我个人习惯把扫描进度通过notifyChannelScanned回调上报给UI层。

// 扫描过程中的回调
public void onScanProgress(int progress) {
    Bundle bundle = new Bundle();
    bundle.putInt("scan_progress", progress);
    notifySessionEvent("scan_progress", bundle);
}
注意:频道扫描是耗时操作,千万别在主线程里做。我曾经见过一个项目,扫描时UI完全卡死,用户以为死机了。一定要用HandlerThread或AsyncTask。

28.2 HDMI CEC:让遥控器“一统天下”

HDMI CEC(Consumer Electronics Control)是个好东西。它让一个遥控器能控制多个设备——电视、机顶盒、功放、蓝光播放器,全都能联动。

举个例子:你按一下电视遥控器的“电源键”,电视关机的同时,机顶盒和功放也跟着关机。这就是CEC的One Touch Standby功能。

28.2.1 CEC的物理层与消息格式

CEC走的是HDMI的14号引脚。消息格式很简单:

  • Header Block:源地址 + 目标地址(4bit + 4bit)
  • Data Block:操作码 + 操作数

比如,0x40 0x44表示“从TV(0x0)发送Standby命令到所有设备(0xF)”。

Android系统里,CEC的驱动层通常由hdmi_cec.ko内核模块处理。应用层通过android.hardware.hdmi.cec这个HAL接口访问。

28.2.2 Android CEC HAL实现要点

实现CEC HAL时,有几个关键点要注意:

  1. 逻辑地址分配:每个设备必须有一个唯一的逻辑地址。TV是0x0,Recording Device是0x1,Tuner是0x3,Playback Device是0x4。
  2. 消息路由:CEC消息是广播式的,但只有目标地址匹配的设备才处理。
  3. 电源状态同步:收到Standby消息后,要触发系统关机流程。
// CEC消息处理示例
void onCecMessageReceived(uint8_t *msg, int len) {
    uint8_t header = msg[0];
    uint8_t src = (header >> 4) & 0x0F;
    uint8_t dst = header & 0x0F;
    uint8_t opcode = msg[1];

    if (opcode == 0x44) { // Standby
        if (dst == 0x0F || dst == getLogicalAddress()) {
            // 触发系统关机
            powerManager.goToSleep();
        }
    }
}

避坑指南:我曾经遇到过CEC消息丢失的问题。排查后发现是HDMI线缆质量太差,14号引脚接触不良。换了一根认证线缆后问题解决。所以,量产时一定要用经过HDMI认证的线缆。

28.3 环绕声直通:让功放“原汁原味”解码

环绕声直通(Audio Passthrough),说白了就是让Android设备把原始的音频码流(比如Dolby TrueHD、DTS-HD MA)直接传给功放,由功放解码。这样能保证音质无损。

为什么需要直通?因为Android系统自带的音频解码器,对高清音频格式支持有限。你想想看,如果电视先解码再PCM输出,那Dolby Atmos的元数据就丢了,环绕效果大打折扣。

28.3.1 直通的实现路径

Android里实现直通,通常走的是HDMI ARC/eARCSPDIF。流程如下:

  1. 应用层通过AudioTrackMediaCodec获取原始音频数据
  2. 数据经过AudioFlinger,直接送到HDMI驱动
  3. HDMI驱动通过I2S或HDA总线,把数据传给功放

关键代码在AudioPolicyManager里:

// 判断是否支持直通
bool isPassthroughSupported(audio_format_t format) {
    switch (format) {
        case AUDIO_FORMAT_E_AC3:     // Dolby Digital Plus
        case AUDIO_FORMAT_E_AC3_JOC: // Dolby Atmos
        case AUDIO_FORMAT_DTS_HD:    // DTS-HD
            return true;
        default:
            return false;
    }
}

// 设置直通模式
status_t setDeviceConnectionState(audio_devices_t device, ...) {
    if (device & AUDIO_DEVICE_OUT_HDMI_ARC) {
        // 启用eARC直通
        mHdmiArc->enablePassthrough(true);
    }
}

28.3.2 直通模式下的EDID协商

直通能不能成功,关键看EDID(Extended Display Identification Data)。EDID里记录了功放支持的音频格式。Android系统在启动时,会读取HDMI接收端的EDID,然后决定哪些格式可以直通。

我建议在HdmiManager里加一个日志,打印EDID的音频块:

void dumpEdidAudioBlock() {
    uint8_t *edid = getEdidData();
    // 解析EDID的Audio Data Block
    for (int i = 0; i < edid[0x7E]; i++) {
        uint8_t format = edid[0x80 + i * 3];
        ALOGI("EDID supports audio format: 0x%02x", format);
    }
}
小技巧:如果功放支持eARC,建议优先走eARC。eARC的带宽高达37Mbps,能传输Dolby TrueHD和DTS-HD MA这种无损格式。而普通ARC只有1Mbps,只能传压缩格式。

28.4 本章小结

TV和机顶盒场景下,TIF、HDMI CEC、环绕声直通这三个模块,是决定用户体验的关键。TIF管的是“看什么”,CEC管的是“怎么控制”,直通管的是“怎么听”。

做系统集成时,我建议你先把TIF的Session生命周期理清楚,再把CEC的逻辑地址分配搞明白,最后用EDID验证直通能力。这三步走稳了,产品的基本盘就稳了。

嗯,这一章就到这里。下一章我们聊聊更底层的音频策略管理,到时候会涉及更复杂的路由和策略配置。


公众号:蓝海资料掘金营,微信 deep3321