第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() {
// 释放资源
}
}
}
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);
}
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时,有几个关键点要注意:
- 逻辑地址分配:每个设备必须有一个唯一的逻辑地址。TV是0x0,Recording Device是0x1,Tuner是0x3,Playback Device是0x4。
- 消息路由:CEC消息是广播式的,但只有目标地址匹配的设备才处理。
- 电源状态同步:收到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/eARC或SPDIF。流程如下:
- 应用层通过
AudioTrack或MediaCodec获取原始音频数据 - 数据经过
AudioFlinger,直接送到HDMI驱动 - 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);
}
}
28.4 本章小结
TV和机顶盒场景下,TIF、HDMI CEC、环绕声直通这三个模块,是决定用户体验的关键。TIF管的是“看什么”,CEC管的是“怎么控制”,直通管的是“怎么听”。
做系统集成时,我建议你先把TIF的Session生命周期理清楚,再把CEC的逻辑地址分配搞明白,最后用EDID验证直通能力。这三步走稳了,产品的基本盘就稳了。
嗯,这一章就到这里。下一章我们聊聊更底层的音频策略管理,到时候会涉及更复杂的路由和策略配置。