Sunday, August 23, 2015

Android TV Framework : HDMI CEC Introduction

Android HDMI-CEC and Tv Input Framework

HDMI CEC feature enables a user to command different electronics devices, CEC enabled and connected through HDMI, with any of their remote control.
Android TV framework provides support for HDMI-CEC features like One Touch Play, Audio Control, Remote Control Pass-through etc.

This is accomplished by HdmiControlService, a system service which interacts with TvInput Framework and AudioSystem.

Full design and integration with TIF can be found on Android official site.

A HDMI input device connected to Android TV, can be controlled by the TV-remote.
The prebuilt TV application will get the all keyEvents and will pass to currently active Session(which is TvInputService). If TvInput Framework has already mapped the corresponding HDMI-ports to CEC logical addresses and these things are stored in corresponding TvInputHardwareInfo, currently active Session on a HDMI port, will dispatch the keys to same port via HdmiControlManager  to hdmi_cec HAL and finally CEC device will respond to command.

Like TvInputService and tv_input HAL dummy implementation, will implement a hdmi_cec HAL and modify our TvInputService to get the keyEvents and pass it as CEC command.

First let's see HDMI CEC service, it's initialization, interaction to HAL and TvInputFramework.

HdmiControlService is a SystemService which creates the HdmiCecController in it's onStart() call and enables OPTION_CEC_SERVICE_CONTROL option.

HdmiControlService::onStart() {

            if (mHdmiControlEnabled) {
                initializeCec(INITIATED_BY_BOOT_UP);

        mCecController = HdmiCecController.create(this);
            mCecController.setOption(OPTION_CEC_ENABLE, ENABLED);
        initPortInfo();
    }

HdmiCecController is the class which interacts to HDMI_CEC Hal through com_android_server_hdmi_HdmiCecController jni.
HdmiCecController::init() registers the HdmiCecController::onReceived callback for device events.

HdmiCecController.java
    static HdmiCecController create(HdmiControlService service) {
        HdmiCecController controller = new HdmiCecController(service);
        long nativePtr = nativeInit(controller, service.getServiceLooper().getQueue());
        controller.init(nativePtr);
    }

    private void init(long nativePtr) {
        mNativePtr = nativePtr;
    }

com_android_server_hdmi_HdmiCecController.cpp
void HdmiCecController::init() {
    mDevice->register_event_callback(mDevice, HdmiCecController::onReceived, this);
}

Now in HdmiControlService::onStart(), initializeCec is called,

HdmiControlService.java
private void initializeCec(int initiatedBy) {
        mCecController.setOption(OPTION_CEC_SERVICE_CONTROL, ENABLED);
        initializeLocalDevices(initiatedBy);
    }

private void initializeLocalDevices(final int initiatedBy) {
                localDevice = HdmiCecLocalDevice.create(this, type);
        allocateLogicalAddress(localDevices, initiatedBy);
    }

private void allocateLogicalAddress(final ArrayList<HdmiCecLocalDevice> allocatingDevices,
            final int initiatedBy) {

            mCecController.allocateLogicalAddress(localDevice.getType(),//call to HAL
                    localDevice.getPreferredAddress(), new AllocateAddressCallback() {
                @Override
                public void onAllocated(int deviceType, int logicalAddress) {//Callback from controller

                        HdmiDeviceInfo deviceInfo = createDeviceInfo(logicalAddress, deviceType,
                                HdmiControlManager.POWER_STATUS_ON);
                        localDevice.setDeviceInfo(deviceInfo);
                        mCecController.addLocalDevice(deviceType, localDevice);
                        mCecController.addLogicalAddress(logicalAddress);
                        allocatedDevices.add(localDevice);
                    }

                        notifyAddressAllocated(allocatedDevices, initiatedBy);
                    }
                }
            });
        }
    }

    private void notifyAddressAllocated(ArrayList<HdmiCecLocalDevice> devices, int initiatedBy) {
        for (HdmiCecLocalDevice device : devices) {
            int address = device.getDeviceInfo().getLogicalAddress();
            device.handleAddressAllocated(address, initiatedBy);
        }
    }

HdmiCecLocalDevice.java
   final void handleAddressAllocated(int logicalAddress, int reason) {
       onAddressAllocated(logicalAddress, reason);
   }

HdmiCecLocalDevice has two type of specialization
1.HdmiCecLocalDeviceTv for local TV type
2.HdmiCecLocalDevicePlayback for a MediaPlayer(CEC enabled) device connected to HDMI input.
And based on type, respective onAddressAllocated callback will be made.
For now let's follow Tv,

HdmiCecLocalDeviceTv::onAddressAllocated(int logicalAddress, int reason) {

       mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
               mAddress, mService.getPhysicalAddress(), mDeviceType));
       mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
               mAddress, mService.getVendorId()));
   
       launchDeviceDiscovery();
   }

HdmiControlService will again notify to  CecController addreses and vendor-id as CECMessage,
HdmiControlService.java
    void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
            mCecController.sendCommand(command, callback);
            }

The CEC features are are encapsulated in multiple type of actions like OneTouchPlayAction,  OneTouchRecordAction, SystemAudioAction, VolumeControlAction, DeviceDiscoveryAction etc. which are started as needed, here first DeviceDiscoveryAction will be started.

HdmiCecLocalDeviceTv.java 
 void launchDeviceDiscovery() { 

       DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
               new DeviceDiscoveryCallback() {
                   @Override
                   public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) {
                       for (HdmiDeviceInfo info : deviceInfos) {
                           addCecDevice(info);
                       }
                       addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));

   }

    final void addCecDevice(HdmiDeviceInfo info) {
        invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
    }


    private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) {

        if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
            mService.invokeDeviceEventListeners(info, status);
        }
    }

And finally HdmiControlService will invoke it's listeners, which is Tv Input Framework.

HdmiControlService::invokeDeviceEventListeners(HdmiDeviceInfo device, int status) {
                    record.mListener.onStatusChanged(device, status);
    }

In next post, we will see how HdmiControlManager.DEVICE_EVENT_ADD_DEVICE event is reached to a TvInputService implementation(like our implementation).

No comments:

Post a Comment