在OTT设备开发中,经常会遇到个设备状态同步联动需求,比如使用电视遥控器开关电视时,把机顶盒也进行同步开关;反之亦然,开关机顶盒时,电视也同步开关,这样使用一个遥控器就可以控制多个设备,提高用户使用体验。这个功能就需要用到HDMI-CEC协议功能。
android官方相关文档可以查看此处:HDMI-CEC
要实现CEC联动设备功能,前提是机顶盒和电视双方都支持CEC功能,不同的制造商实现上可能有差异,要根据具体设备来看。同时要区分源端和目标端,通常机顶盒等播放设备是源端,电视等显示设备是目标端,固件中通过build.prop属性区分:
源端:
PRODUCT_PROPERTY_OVERRIDES += ro.hdmi.device_type=4
目标端:
PRODUCT_PROPERTY_OVERRIDES += ro.hdmi.device_type=0
应用中需要添加cec权限:
<?xml version="1.0" encoding="utf-8"?>
<permissions>
<permission name="android.permission.HDMI_CEC" >
<group gid="system"/>
</permission>
</permissions>
我是基于海思Hi3798MV200方案进行开发的,下面均以此平台sdk源码为例简单介绍一下CEC联动过程。
应用层主要关注两个类:HdmiControlManager HdmiControlService
源码路径:HiSTBAndroidV600R003C00SPC020\frameworks\base\core\java\android\hardware\hdmi\HdmiControlManager.java
HiSTBAndroidV600R003C00SPC020\frameworks\base\services\core\java\com\android\server\hdmi\HdmiControlService.java
所有的应用,都会间接通过HDMIControlManager或者输入通过Tv Input框架间接与HdmiControlService进行通信,HdmiControlService作为SystemServer服务的一个服务,负责处理CEC的命令并与HDMI-CEC HAl进行交互。HAL层和驱动都需要厂商去适配,最后通过CEC总线与CEC设备通信。
基本流程:获取HdmiControlManager对象,通过该对象调用HdmiControlService中的相关方法-检测设备类型源端--检测CEC功能支持--注册监听回调--发送控制命令。
简易示例:
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiPlaybackClient;
// 获取HdmiControlManager实例
HdmiControlManager hdmiControlManager = (HdmiControlManager) getSystemService(Context.HDMI_CONTROL_SERVICE);
// 检查HDMI CEC是否可用
if (hdmiControlManager.isAvailable()) {
// 获取HdmiPlaybackClient实例(源端)
HdmiPlaybackClient hdmiPlaybackClient = hdmiControlManager.getPlaybackClient();
// 获取HdmiTvClient实例(目标端)
// HdmiTvClient hdmiTvClient = hdmiControlManager.getTvClient();
// 检查TV是否处于开机状态
if (hdmiControlManager.getDevicePowerStatus() != HdmiControlManager.POWER_STATUS_ON) {
// 发送开机命令
hdmiPlaybackClient.sendStandby();
} else {
// 发送关机命令
hdmiPlaybackClient.sendPowerOff();
}
}
CEC除了开关机同步,也可以实现各按键功能同步,比如上下左右等功能,还可以设置音量等,除了调用封装好的功能函数,也可以采用发送指令码方式,不同厂商方式有差异,比如海思平台的:
@Override
public void setSystemAudioVolume(final int oldIndex, final int newIndex,
final int maxIndex) {
enforceAccessPermission();
runOnServiceThread(new Runnable() {
@Override
public void run() {
HdmiCecLocalDeviceTv tv = tv();
if (tv == null) {
Slog.w(TAG, "Local tv device not available");
return;
}
tv.changeVolume(oldIndex, newIndex - oldIndex, maxIndex);
}
});
}
@Override
public void setSystemAudioMute(final boolean mute) {
enforceAccessPermission();
runOnServiceThread(new Runnable() {
@Override
public void run() {
HdmiCecLocalDeviceTv tv = tv();
if (tv == null) {
Slog.w(TAG, "Local tv device not available");
return;
}
tv.changeMute(mute);
}
});
}
@Override
public void setArcMode(final boolean enabled) {
enforceAccessPermission();
runOnServiceThread(new Runnable() {
@Override
public void run() {
HdmiCecLocalDeviceTv tv = tv();
if (tv == null) {
Slog.w(TAG, "Local tv device not available to change arc mode.");
return;
}
}
});
}
@Override
public void setProhibitMode(final boolean enabled) {
enforceAccessPermission();
if (!isTvDevice()) {
return;
}
HdmiControlService.this.setProhibitMode(enabled);
}
@Override
public void addVendorCommandListener(final IHdmiVendorCommandListener listener,
final int deviceType) {
enforceAccessPermission();
HdmiControlService.this.addVendorCommandListener(listener, deviceType);
}
@Override
public void sendVendorCommand(final int deviceType, final int targetAddress,
final byte[] params, final boolean hasVendorId) {
enforceAccessPermission();
runOnServiceThread(new Runnable() {
@Override
public void run() {
HdmiCecLocalDevice device = mCecController.getLocalDevice(deviceType);
if (device == null) {
Slog.w(TAG, "Local device not available");
return;
}
if (hasVendorId) {
sendCecCommand(HdmiCecMessageBuilder.buildVendorCommandWithId(
device.getDeviceInfo().getLogicalAddress(), targetAddress,
getVendorId(), params));
} else {
sendCecCommand(HdmiCecMessageBuilder.buildVendorCommand(
device.getDeviceInfo().getLogicalAddress(), targetAddress, params));
}
}
});
}
@Override
public void sendStandby(final int deviceType, final int deviceId) {
enforceAccessPermission();
runOnServiceThread(new Runnable() {
@Override
public void run() {
HdmiMhlLocalDeviceStub mhlDevice = mMhlController.getLocalDeviceById(deviceId);
if (mhlDevice != null) {
mhlDevice.sendStandby();
return;
}
HdmiCecLocalDevice device = mCecController.getLocalDevice(deviceType);
if (device == null) {
Slog.w(TAG, "Local device not available");
return;
}
device.sendStandby(deviceId);
}
});
}
常见的CEC协议指令码:
HDMI-CEC命令是通过特定的操作码(Opcode)来实现的,每个操作码对应一个特定的功能。以下是一些常用的HDMI-CEC命令及其对应的操作码:
1. **One Touch Play**(一键启动)
- `0x04`:Image View On
- `0x34`:Tuner Step Increment (used for one touch record start)
- `0x35`:Tuner Step Decrement (used for one touch record stop)
2. **System Standby**(待机)
- `0x36`:Standby
3. **Preset Transfer** (不是一个典型的CEC命令,通常涉及到多个命令和状态信息的交换)
4. **One Touch Record**
- `0x37`:Record On
- `0x38`:Record Status
- `0x39`:Record Off
5. **Timer Programming** (涉及多个命令,如设置定时器信息)
- `0x97`:Timer Cleared Status
- `0x43`:Timer Status
6. **System Information**
- `0x83`:Give Physical Address
- `0x84`:Report Physical Address
- `0x85`:Request Active Source
- `0x86`:Set Stream Path
7. **Deck Control**
- `0x42`:Deck Control
- `0x1B`:Deck Status
8. **Tuner Control**
- `0x54`:Tuner Device Status
- `0x55`:Give Tuner Device Status
- `0x67`:Tuner Step Increment
- `0x68`:Tuner Step Decrement
9. **Vendor Specific Commands**
- `0x89`:Vendor Command
- `0x8C`:Vendor Remote Button Down
- `0x8D`:Vendor Remote Button Up
- `0x8E`:Give Device Vendor ID
- `0x8F`:Menu Request
10. **Menu Navigation**
- `0x8B`:User Control Pressed
- `0x8C`:User Control Released
- `0x8D`:Give Device Power Status
- `0x8E`:Report Power Status
- `0x0D`:Menu Request
- `0x32`:Set Menu Language
请注意,这些操作码仅是CEC协议中的一部分,而且不同厂商可能会有不同的实现或扩展。要完整地实现CEC功能,还需要考虑具体的设备逻辑地址、参数传递和状态机制等因素。而且,由于CEC协议的实现可能因厂商而异,有时候相同的操作码在不同设备间可能会有不同的行为。如果你需要更详细的信息,通常需要参考HDMI规范的官方文档。
我在实现过程中,通过上述应用层方法没有走通,CEC模块检测通不过,但实际是支持的,好在源码环境开发,我就直接到底层JNI代码中将检测条件去掉了,直接在底层实现相关功能,应用层方法最终也还是调用底层JNI方法的。
HAL层源码位置:
HiSTBAndroidV600R003C00SPC020\device\hisilicon\bigfish\frameworks\hidisplaymanager\hal\hi_adp_hdmi.c
这个是所有HDMI相关控制的中间件层位置,比如分辨率设置、热插拔检测、CEC控制等