Android HDMI-CEC实现机顶盒与电视联动(共用遥控器)

在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控制等

猜你喜欢

转载自blog.csdn.net/HuanWen_Cheng/article/details/134339211