miracast uibc详解

在这里插入图片描述
如上图所示,官方wifi display参考架构里有一个uibc的功能一直没用上。目前已知的信息是,这个是用来做反向控制的一个功能,在电视上即可对手机进行控制。我们现在就来研究一下

1. UIBC定义

User Input Back Channel
用户输入反向控制通道(UIBC)是一个可选的WFD特性,实现该扩展功能,有助于用户从WFD sink端控制WFD source端的通信。
wifi display spec文档定义:

4.11 User Input Back Channel The User Input Back Channel (UIBC) is an optional WFD feature that when implemented facilitates communication
of user inputs to a User Interface, present at the WFD Sink, to the
WFD Source. All UIBC user inputs are packetized using a common packet
header and transported over TCP/IP. The user input categories include
Generic, and HIDC. The Generic category is used for device agnostic
user inputs that are processed at the application level. Generic user
inputs are formatted using the Generic Input Body. HIDC is used for
user inputs generated by HIDs like remote control, keyboard, etc.[46].
HIDC user inputs are formatted using the HIDC Input Body. 翻译一下
4.11 UIBC UIBC是WFD的可选feature,便于用户在UI上用户交流?展现在sink端,到source端?什么鬼! 所有uibc用户输入被打包成common的包头,然后用tcp/ip进行传输,user input类别包括HIDC和Generic。
Generic类别被用于在应用程序级别处理的与设备无关的用户输入。 HIDC被用于通过HIDs(一类HID类设备)生成的user
input,像远程控制,键盘等等,UIDC user inputs被用UIDC Input Body来格式化

虽然翻译的乱七八糟,但还是大概有了点概念。
接下来是

1.1 UIBC的TCP/IP封包形式:

在这里插入图片描述
The fields in the common packet header are described below:

  • Version (3 bits):
    The version of the protocol. This field shall be set to 0b000.
  • T (1 bit):
    Presence of the optional timestamp before the input body, where 0 means the timestamp field does not exist, and 1 means the timestamp field exists.猜测这是一个事件对应的时间戳,可以设置0.时间就不设置了
  • Reserved (8 bits):
    预留了8bits
  • Length (16 bits):
    整个 TCP 有效负载的长度,以 8 位为单位,从位偏移量 0 到 UIBC 输入正文的结尾
    (包括填充,如果有的话)。
  • Input Category (4 bits):
    就是前面提到的Generic或者HIDC,前者用0,后者用1.用来表明当前这条UIBC的类型是哪个。
  • UIBC Input Body:
    此字段是Generic输入正文或 HIDC 输入正文,如输入类别字段中所示,包含
    描述一个或多个用户输入的信息。 一个用户输入对应一个Generic输入消息或一个 HIDC 消息,具体
    取决于所选的输入类别。 此外,该字段应填充为整数16 位的倍数,以便在 Length 字段中具有偶数。

2. UIBC的建立和维护

UIBC是使用RTSP GET_PARAMETER 和SET_PARAMETER消息建立和维护的。
消息序列如下两个Figure
在这里插入图片描述
在这里插入图片描述
WFD Source用于 UIBC 消息事务的 TCP 端口号包含在 wfd-uibc-capability 中RTSP M4
和/或 M14 请求消息中的参数,并且 WFD Source上的该端口应准备好接受在发送后续
RTSP M4 和/或 M14 请求消息之前从 WFD Sink传入的连接包含 wfd-uibc-capability 参数。
一旦建立,WFD Source和 WFD Sink之间的单个 TCP 连接WFD Sink应在 WFD 会话期间
用于所有 UIBC 数据交换。

2.1 UIBC Input Body

2.1.1 Generic input Body Format:

在这里插入图片描述
类型那里,有鼠标/Touch按下抬起,鼠标移动,放大,滚动,旋转等等,具体参考
《Wi-Fi_Display_Technical_Specification_v2.1_0》表15即可。第三列的Describe这里基本就是显示区域的坐标了。

2.1.2 HIDC Input Body Format

在这里插入图片描述
HIDC Input Path如表23所示,有红外线,USB,蓝牙,Zigbee,Wi-Fi等
HID Type有键盘鼠标游戏手柄,相机,手势,远程控制等

3. source端实例分析

github中 hw手机fw代码uibc部分研究
代码如下:反编译出来的代码
https://github.com/SivanLiu/HwFrameWorkSource/blob/5b92ed0f1ccb4bafc0fdb08b6fc4d98447b754ad/Mate20_9_0_0/src/main/java/com/android/server/display/HwUibcReceiver.java(疑似华为手机source uibc的实现)

3.1 创建UIBC接收器

WifiDisplayController.this.mUibcInterface.createReceiver,调用的是HwUibcReceiver中的createReceiver方法
在这里插入图片描述
由于这个HwUibcReceiver继承自HandlerThread,所以这里调用的start方法会启动线程.线程里做什么还没找到。
然后调用了CreateHandler方法,初始化了mHandler为UibcHandler
在这里插入图片描述
msg为1的时候,干的事情大概是初始化的,log可以看出是UIBC_START了。并且在后面有调用
Socket unused2 = HwUibcReceiver.this.mSocket = HwUibcReceiver.this.mServer.accept();
做一个等待连接
socket的初始化在前面HwUibcReceiver构造函数中:
在这里插入图片描述
有连接过来之后使用这条socket初始化了mInput
BufferedInputStream unused4 = HwUibcReceiver.this.mInput = new BufferedInputStream(HwUibcReceiver.this.mSocket.getInputStream());
然后便是
HwUibcReceiver.this.receiveEvent();
在这里插入图片描述
发了msg what为2.即是
在这里插入图片描述
主要就是read从socket收到的数据,然后consumePacket进行消费,然后接着调用receiveEvent。
大概流程清楚了。
那我们接着分析消费的流程。消费 = 解析 + 处理

3.2 消费socket包consumePacket

在这里插入图片描述
跟协议相对应,先看当前的input category是哪种,前面协议部分已经说过了,0是generic事件,1是hid事件。很遗憾,这里hid调用了native方法,而对应的native代码肯定是无法被反编译的。那这里就先不分析了。我们先研究Generic Input

3.3 处理Generic Input

在这里插入图片描述
对前文提到的Generic类型的input有所呼应。这个函数大概做的事情

3.4 解析输入正文resolveInputBody

在这里插入图片描述
payload的第一位表明了inputType.协议table15中有定义,按下抬起,滚动,缩放等。
后面2位计算出bodyLength之后向前移动三字节。最后面那个mParseBytes应该是全局记录当前解析的位置的变量。

3.5 解析事件resolveEvent

在这里插入图片描述
根据inputType分别调用createKeyEvent或者createTouchEvent来创建不同的InputEvent对象。
我们先看简单的

3.5.1 创建key事件createKeyEvent

在这里插入图片描述
请注意这里new的KeyEvent对象其实继承自InputEvent.
这里调用的构造函数是这个:
在这里插入图片描述
按下时间点,事件发生时间点,action这个大家都很熟悉了,code也是,调用者传入的
b = payload[index + 5];说明偏移5个字节(这里为什么偏移5?)指示的就是事件的code,即这些:
在这里插入图片描述

3.5.2 创建touch事件createTouchEvents

在这里插入图片描述
遍历payload,然后调用doTranslateCoord翻译坐标,虽然这里传入参数和解析过程应该都很重要,但是看着比较复杂。简单来说就是根据sink传过来的左边做了一个坐标点的转换。

3.6 注入input事件injectInputEvent

遍历所有的Event,然后分别调用InputManager的injectInputEvent接口,注入事件
在这里插入图片描述
也就是说构造一个InputEvent对象送进入,只要这个event合理合法,系统就会作出响应。

4. sink端实例分析(控制台版本)

代码地址:
https://github.com/albfan/miraclecast/tree/master/src/uibc
看上去只是一个控制台的二进制实现,并不是电视端的,但是大致原理应该是相通的
在这里插入图片描述

4.1 socket连接

在这里插入图片描述
输入两个参数,参数1是ip地址,参数2是端口
在这里插入图片描述
创建一路TCP连接,跟前面source端对应起来理解
UIBC这种case里,被反向控制的设备是server,而大屏端其实是client

  if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
    perror("ERROR connecting");
    return EXIT_FAILURE;
  }

然后接收控制台输入事件

	bzero(buffer, 256);
    fgets(buffer, 255, stdin);
    char type = buffer[0];

    UibcMessage uibcmessage;
    // 0 1 3 4定义在spec里 table 15
    if (type == '0' || type == '1') {
      uibcmessage = buildUibcMessage(GENERIC_TOUCH_DOWN, buffer, 1, 1);
    } else if (type == '3' || type == '4') {
      uibcmessage = buildUibcMessage(GENERIC_KEY_DOWN, buffer, 1, 1);
    } else {
      if (!daemon) {
        printf("unknow event type: %s", buffer);
      }
      continue;
    }

4.2 打包Input Body

组包过程如下
// UibcMessage结构体定义
typedef struct {
   char* m_PacketData;
   size_t m_PacketDataLen;
   bool m_DataValid;
} UibcMessage;

UibcMessage buildUibcMessage(MessageType type,
    const char* inEventDesc,
    double widthRatio,
    double heightRatio) {
  UibcMessage uibcmessage;
  uibcmessage.m_PacketData = NULL;
  uibcmessage.m_PacketDataLen = 0;
  uibcmessage.m_DataValid = false;

  switch (type) {
    case GENERIC_TOUCH_DOWN:
    case GENERIC_TOUCH_UP:
    case GENERIC_TOUCH_MOVE:
      getUIBCGenericTouchPacket(inEventDesc,
          &uibcmessage,
          widthRatio,
          heightRatio);
      break;

    case GENERIC_KEY_DOWN:
    case GENERIC_KEY_UP:
      getUIBCGenericKeyPacket(inEventDesc, &uibcmessage);
      break;

    case GENERIC_ZOOM:
      getUIBCGenericZoomPacket(inEventDesc, &uibcmessage);
      break;

    case GENERIC_VERTICAL_SCROLL:
    case GENERIC_HORIZONTAL_SCROLL:
      getUIBCGenericScalePacket(inEventDesc, &uibcmessage);
      break;

    case GENERIC_ROTATE:
      getUIBCGenericRotatePacket(inEventDesc, &uibcmessage);
      break;
  };
  return uibcmessage;
}
根据不同的type构建不同的uibcmessage。我们先看一个看上去更简单的getUIBCGenericKeyPacket(touch还要考虑坐标,相对复杂一些)
  UibcMessage* uibcmessage) {
    char* outData = uibcmessage->m_PacketData;
    int32_t typeId = 0;
    int32_t uibcBodyLen = 0;
    int32_t genericPacketLen = 0;
    int32_t temp = 0;
    size_t size;
    // 这个inEventDesc从前面看,是由控制台输入的字符串
    // 从这里来看,应该是使用逗号分割开的一堆字符串
    // 并且str_split函数还算出了事件的个数
    char** splitedStr = str_split((char*)inEventDesc, ",", &size);
    if (size > 0) {
        // 这里说明一个事件至少三段
        // typeId, key Code,另外一个是啥?
        if (((int)size) % 3 != 0) {
            log_error("getUIBCGenericKeyPacket (%s)", "bad input event");
            return;
        }
        //由于splitedStr是一个指针数组,所以
        //i++之后
        //splitedStr + i就指向了下一个逗号隔开的元素了
        int i;
         for (i = 0; i < size; i++) {
             switch (i) {
            case 0: {
                // 第一段是一个整数,typeid
                typeId = atoi(*(splitedStr + i));
                genericPacketLen = 5;
                uibcBodyLen = genericPacketLen + 7; // Generic header legth = 7
                // 总共13字节的buffer
                outData = (char*)malloc(uibcBodyLen + 1);
                // UIBC header
                outData[0] = 0x00; //Version (3 bits),T (1 bit),Reserved(8 bits),Input Category (4 bits)
                outData[1] = 0x00;
                // 低八位放第一个字节
                outData[2] = (uibcBodyLen >> 8) & 0xFF; //Length(16 bits)
                // 高八位放在第二字节
                outData[3] = uibcBodyLen & 0xFF; //Length(16 bits)
                //Generic Input Body Format
                outData[4] = typeId & 0xFF; // Type ID, 1 octet
                outData[5] = (genericPacketLen >> 8) & 0xFF; // Length, 2 octets
                outData[6] = genericPacketLen & 0xFF; // Length, 2 octets
                outData[7] = 0x00; // reserved
                break;
            }
            case 1: {
                sscanf(*(splitedStr + i), " 0x%04X", &temp);
                if (temp == 0) {
                    outData[8] = 0x00;
                    outData[9] = 0x00;
                    //这里不是应该直接return吗?
                }
                // 填充keyCode
                // 没猜错的话,这个keyCode应该完全对应Android keyevent中定义的那一套
                log_info("getUIBCGenericKeyPacket key code 1=[%d]\n", temp);
                outData[8] = (temp >> 8) & 0xFF;
                outData[9] = temp & 0xFF;
                break;
            }
            case 2: {
                sscanf(*(splitedStr + i), " 0x%04X", &temp);
                if (temp == 0) {
                    outData[10] = 0x00;
                    outData[11] = 0x00;
                }
                outData[10] = (temp >> 8) & 0xFF;
                outData[11] = temp & 0xFF;
                break;
            }
             default: {
                 }
                 break;
         }
         free(*(splitedStr + i));
    }
}

free(splitedStr);
uibcmessage->m_DataValid = true;
uibcmessage->m_PacketData = outData;
uibcmessage->m_PacketDataLen = uibcBodyLen;

4.3 发送带input body的uibc包

r = sendUibcMessage(&uibcmessage, sockfd);
if (r) {
      return r;
}
close(sockfd);

前面组完包之后就调用sendUibcMessage进行发送,第二个参数为4.1中创建的socket连接fd。

int sendUibcMessage(UibcMessage* uibcmessage, int sockfd) {
  ssize_t n;

  printf("sending %zu bytes\n", uibcmessage->m_PacketDataLen);

  n = write(sockfd, uibcmessage->m_PacketData , uibcmessage->m_PacketDataLen);

  if (n < 0) {
    perror("ERROR writing to socket");
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}

到这一步,基本上,发送和接收都完成了一遍。只不过,接收是华为手机,为android系统,发送的应该不是跑早Android上,还需要做一些移植。接下来就需要在Android电视上做一个实际的尝试。
结果又让我发现了一个疑似android sink的代码实现,赶紧继续分析一下!!!!

5. Android sink实例分析

发现一个很全面的例子
https://github.com/LIFECorp
可以从上到下捋一遍了

5.1 frameworks/base

提供了一个应用接口:

 /**
     * M:[UIBC] Providing application API to send input event via UIBC
     * 
     * @param input event
     * @hide
     */
    public void sendUibcInputEvent(String input) {
        if (FeatureOption.MTK_WFD_SINK_UIBC_SUPPORT) {
            mGlobal.sendUibcInputEvent(input);
        }
    }

5.2 frameworks/av

/av/media/libmediaplayerservice/RemoteDisplay.cpp
status_t RemoteDisplay::sendUibcEvent(const String8& eventDesc) {
    const char* pEventDes = eventDesc.string();
    // 取一个字节。直接转换
    int type = *pEventDes;
    if (*(pEventDes+1) != ',')
        return BAD_VALUE;

    ALOGD("sendUibcEvent: type=0x%X", type);

    switch (type) {
    //0x30是数字0字符的ascii码
    case 0x30:
    case 0x31:
    case 0x32:
        mSink->sendUIBCGenericTouchEvent(pEventDes);
    break;
    //0x33是数字3字符的ascii码
    case 0x33:
    case 0x34:
        mSink->sendUIBCGenericKeyEvent(pEventDes);
    break;
    default:
        return BAD_VALUE;
    }
    return OK;
}

5.4 frameworks-ext/av

status_t WifiDisplaySink::sendUIBCGenericTouchEvent(const char * eventDesc) {
    status_t err;
    if (mUibcClientHandler == NULL) return -1;
    err = mUibcClientHandler->sendUibcMessage(mNetSession,
            UibcMessage::GENERIC_TOUCH_DOWN,
            eventDesc);
    return err;
}

到这里就很全面了,过程推测应该和前面4中控制台的基本一样。毕竟函数名字都一模一样。也不知道谁抄的谁。
还有个遗留问题,应用是怎么调用的sendUibcInputEvent接口。找到的结果如下

5.5 packages/apps/Settings

// /src/com/mediatek/settings/wfd/WfdSinkSurfaceFragment.java
@Override
public boolean onTouchEvent(MotionEvent ev) {
    ...
    StringBuilder eventDesc = new StringBuilder();
    eventDesc.append(
        //如果是uo的时候,这里就用
        //GENERIC_INPUT_TYPE_ID_TOUCH_UP
        //这里就不用ascii码了
        //因为0转换完之后就是0x30了
        String.valueOf(GENERIC_INPUT_TYPE_ID_TOUCH_DOWN))
        .append(",");
    eventDesc.append(getTouchEventDesc(ev));
    sendUibcInputEvent(eventDesc.toString());
    ...
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    ...
    int asciiCode = event.getUnicodeChar();
    if (asciiCode == 0 || asciiCode < 0x20) {
        Xlog.d(TAG, "Can't find unicode for keyCode=" + keyCode);
        asciiCode = KeyCodeConverter.keyCodeToAscii(keyCode);
    }
    StringBuilder eventDesc = new StringBuilder();
    eventDesc
        .append(String.valueOf(GENERIC_INPUT_TYPE_ID_KEY_DOWN))
        .append(",").append(String.format("0x%04x", asciiCode))
        .append(", 0x0000");
    sendUibcInputEvent(eventDesc.toString());
    ...
}

终于知道这个eventDesc是怎么构造的了

  1. 0x30/0x31表示touch down/touch up, 0x33/0x34表示key down/key up
  2. keyCode转换成ascii码,比如home键的keycode是3。ascii码是0x33。那这里就应该是0x0033
  3. 再补上一个0x0000就完了。估计这里是同时可以传两个keyCode?
    那如果要传递home键按下抬起,正确的eventDesc应该是"0x33,0x0033,0x0000"以及"0x34,0x0033,0x0000"。

6. HIDC

前面我们完成了手机的反控,除了鸿蒙手机之外,其他手机都可以通过generic input的方式进行反向控制。但是我们发现,无法通过uibc控制windows pc。
原因在这个前辈的博客里有提到:

https://blog.csdn.net/sinat_37343534/article/details/116446980
目前Android设备一般支持generic,Win10支持HIDC协议。也有特例,比如荣耀的手机默认支持HIDC类型的UIBC同时也支持generic协议。

另外,下面这个内容来自microsoft官网的介绍
在这里插入图片描述
https://docs.microsoft.com/en-us/windows-hardware/drivers/hid/virtual-hid-framework–vhf-
简单来说,是需要通过数据流的方式,发送"HID报告"给到windows系统,让windows系统知道你想干什么?
关于hid报告,hid report descriptor.找到的内容多是usb开发相关的。太过复杂。
先采取一个思路,先看mtk对于hid事件的解析,去推测hid事件的封装。

7.1 Mtk hid事件解析源码分析

源码路径:
/rtsp_if/src/WfdRtspProtocol.cpp
先以鼠标事件作为突破口

/* ============================ HIDC ===============================*/
else if (WfdRtspProtocol::instance().mUibcEventCategory == UIBC_CATEGORY_HIDC) {
    WfdRtspUibcPacket* newPacket = new WfdRtspUibcPacket();
    currEventType = WfdRtspProtocol::instance().mUibcEventType;
    newPacket->setCategory(WfdRtspUibcPacket::UIBC_HIDC);
    if (WfdRtspProtocol::instance().mUibcEventType == HIDC_EVT_TYPE_MOUSE)
        newPacket->setHidType(WfdRtspUibcPacket::HID_TYPE_MOUSE);
    else
        newPacket->setHidType(WfdRtspUibcPacket::HID_TYPE_KEYBOARD);
    UibcQueue.push_back(newPacket);
    ...
    WfdRtspUibcPacket* uibcPacket = UibcQueue.front();
    UibcQueue.pop_front();
    if (WfdRtspProtocol::instance().mUibcEventType == HIDC_EVT_TYPE_MOUSE) {
        /* --------------------------------------------------------------------
                                        HID Mouse/USB
        -------------------------------------------------------------------*/
        // displacement是移位的意思
        int disps[2] = {5, 5}; /* x-displacement, y-displacement */
        // 循环30次
        for (round_count = 0; round_count < DEFAULT_ROUNDS_MOUSE; round_count ++)
        {
            sendLen = uibcPacket->getTranData(buffer, UIBC_BUFFER_SIZE, (void *)disps);
            if(send(uibcSocket, buffer, sendLen, 0) < 0) {
                WFD_LOG_ERR("Error! send UIBC Mouse data1 error !");
                break;
            }
            total_counts ++;
            disps[0] += 5;
            disps[1] += 5;
            // sleep 500ms
            usleep(DELAY_BETWEEN_EACH_EVENT_MS * 1000);
            ...
        }
    }    
}

这段代码主要是循环发送了一个鼠标事件,坐标值从5,5一直自增加5,增加三十次,看上去是轨迹是一个斜率为1的一条斜线。
继续看下这个关键方法getTranData,这个方法完成的就是组包操作

int WfdRtspUibcPacket::getTranData(unsigned char * buffer,int len, void *data)
{
    ...
    else if (mCategory == UIBC_HIDC)
    {
        struct uibc_packet_s upkt;
        unsigned short *usptr;
        unsigned char *buf_ptr = NULL, *buf_head_ptr = NULL;
        memset(buffer, 0, len);
        memset(&upkt, 0, sizeof(struct uibc_packet_s));
        
        // 构造包头
        upkt.hdr.version = mVersion;
        upkt.hdr.T = 0;
        upkt.hdr.rsvd = 0;
        upkt.hdr.input_category = mCategory;
        // 转换网络顺序
        /* need to convert first ushort bytes to network order */
        usptr = (unsigned short *)&upkt.hdr;
        *usptr = htons(*usptr);
        if (mHidType == HID_TYPE_MOUSE)
        {
            struct hid_mouse_input_report_s *mouse = NULL;
            //坐标指针
            int *disps = (int *)data;
            // 必须走USB?
            // 其他选项有INFRARED红外线
            // BT WIFI等等
            // 我理解这里是采用数据流模拟USB输入的意思
            upkt.u_body.uibc_hidc.input_path = HID_INPUT_PATH_USB;
            upkt.u_body.uibc_hidc.hid_type = HID_TYPE_MOUSE;
            upkt.u_body.uibc_hidc.usage = 0; /* not Report Descriptor */
            upkt.u_body.uibc_hidc.length = htons(HIDC_USB_MOUSE_VALUE_LEN);
            // 构造鼠标hid report descriptor
            // 从这里来看,mouse直接指向了hidc_value的首地址
            mouse = (struct hid_mouse_input_report_s *)&upkt.u_body.uibc_hidc.hidc_value[0];
            mouse->x_disp = (unsigned char)disps[0];
            mouse->y_disp = (unsigned char)disps[1];
            // 这里提示要注意字节对齐的问题
            /* copy result to buffer */
            /* Note that because alignment issue that after HIDC "usage filed" will have one additional
               byte, so we manage the buffer by copying.
               It is better to use pragma though.
            */
            buf_ptr = buffer;
            buf_head_ptr = buf_ptr;
            // 跳过uibc包头
            buf_ptr += sizeof(struct uibc_packet_hdr_s);
            *buf_ptr++ = upkt.u_body.uibc_hidc.input_path;
            *buf_ptr++ = upkt.u_body.uibc_hidc.hid_type;
            *buf_ptr++ = upkt.u_body.uibc_hidc.usage;
            memcpy(buf_ptr, (unsigned char *)&upkt.u_body.uibc_hidc.length, sizeof(upkt.u_body.uibc_hidc.length));
            // 跳到hidc
            buf_ptr += sizeof(upkt.u_body.uibc_hidc.length);
            // 填充hid report descriptor
            memcpy(buf_ptr, mouse, sizeof(struct hid_mouse_input_report_s));
            buf_ptr += sizeof(struct hid_mouse_input_report_s);
            // 4+9
            retLen = UIBC_HDR_LEN + UIBC_HIDC_BODY_LEN_MOUSE;
            // 主机字节序变为网络字节序
            // 网络字节顺序采用big-endian排序方式
            upkt.hdr.length = htons((unsigned short)retLen);
            memcpy(buf_head_ptr, (unsigned char *)&upkt.hdr, sizeof(struct uibc_packet_hdr_s));
        }
    }
}

上面这些复杂的结构体看名字头都晕。可以看下定义

struct uibc_packet_s
    {
        struct uibc_packet_hdr_s hdr;
        // 这里是二选一的意思
        union
        {
            struct uibc_body_generic_s uibc_gen;
            struct uibc_body_hidc_s uibc_hidc;
        } u_body;
    };
    
    struct uibc_packet_hdr_s
    {
        // 冒号这是c语言的写法,位域
        // 表示占用4位
        unsigned short input_category : 4; /* Input Category: b12~15 */
        unsigned short rsvd : 8; /* Reserved: b4~11 */
        unsigned short T : 1; /* T: b3 */
        unsigned short version : 3; /* version: b0~2 */
        unsigned short length; /* Length: b16~31 */
        /* unsigned short timestamp; */ /* Timestamp(Optional): b32~47*/
    };
    // 这个是generic的
    struct uibc_body_generic_s
    {
        unsigned char input_type_id;
        unsigned short length;
        unsigned char describe[64];
    };
    
    // 这一部分才是属于hidc的
    struct uibc_body_hidc_s
    {
        unsigned char input_path;
        unsigned char hid_type;
        unsigned char usage;
        unsigned short length;
        // 如果是描述符这种形式,hid_mouse_input_report_s是直接放在这里的
        unsigned char hidc_value[64];
    };
    
    struct hid_mouse_input_report_s
    {
        unsigned char buttons;
        unsigned char x_disp;
        unsigned char y_disp;
        unsigned char dev_specific;
    };

到这里,理论上来说功能已经可以实现了。按照固定格式组好13个字节(头一共32位,四个字节。body9个字节uibc_body_hidc_s由1+1+1+2+4(report desc)组成)的包,然后通过socket发送出去应该就好了。

7.2 实际开发中的坑

实际开发下来,发现hid_mouse_input_report_s这个结构体,后面dev_specifics字段应该拿掉。参考:
在这里插入图片描述
在这里插入图片描述
图片来自https://blog.csdn.net/sinat_37343534/article/details/116446980
修改完之后,windows10电脑确实响应了。但是位置不对。坐标各种乱跳。毕竟x,y坐标每个只有一个字节的位置。0-255的范围肯定无法和1920*1080分辨率的电视做对应的。
到这里我突然想起在哪里看到过,好像可以在某一位指定坐标值是否超过255。
在这里插入图片描述

搜索之后发现确实有这个说法。也就是说前面一个字节的button其实八个byte都是有意义的。又去翻了下Wi-Fi_Display_Technical_Specification_v2.1_0,又发现点新东西
在这里插入图片描述
大概意思是说:
HID 报告描述符描述了其关联的 HID 输入报告的格式。 对于每个 HID 接口类型和 HID 类型组合,WFD 接收器应在发送 HID 输入之前将其关联的 HID 报告描述符发送到 WFD 源向 WFD 源报告。 WFD Sink 可以在多个场合向 WFD Source 发送 HID 报告描述符以确保 WFD 源具有最新的 HID 报告描述符。
指定了 USB 键盘和鼠标 HID 输入报告的默认描述符。 如果 HID 输入报告 WFD 接收器发送基于 USB 键盘和鼠标的默认报告描述符,WFD 接收器不需要发送HID 报告描述符。 键盘的默认 USB HID 报告描述符设置为 E.6 节中指定的描述符[21] 中的“报告描述符(键盘)”。 鼠标的默认 USB HID 报告描述符设置为在[21] 中的 E.10 节“报告描述符(鼠标)”。
原来前面截图中的e10来自这里(Wi-Fi_Display_Technical_Specification_v2.1_0引用的一个文档):
[21] “Universal Serial Bus Specification - USB Device Class Definition for Human Interface Devices”, Version 1.11, Jun.2001 (文档地址文后有附)。
这里有个疑问,如果usage设置0x01.不发送报告描述符,那坐标如何送给source?
结果,我又看了一下usage的描述
This field indicates the usage of the HIDC value field in this table. The value of
this field shall be set to 0x00 if the HIDC value field contains a HID input report.
The value of this field shall be set to 0x01 if the HIDC value field contains a HID
report descriptor.
仔细看了,才发现
0x00 hidc value field包含的是hid input report
0x01 hidc value field包含的是hid report desctiptor
而前面又说到E.10表格就已经是默认report desctiptor。也就是说如果设置0x00那就直接使用E.10中默认好的report descriptor,自己无需再做复杂的封装。而从https://blog.csdn.net/sinat_37343534/article/details/116446980
这位前辈的探索来看,只需在descriptor的input位置提供值即可。
继续研究这个坐标值的问题,前述图片中有提到x,y只是坐标变化量。怪不得我每次点击同一个位置,好像鼠标指针的确是按照固定的步进去移动。

7.3实验结果

  1. button位符号位字节没用,设置了不识别,直接在坐标位置赋值实际计算出来的正/负delta就可以。
  2. button位指示三个button按下的位置有用,如果不设置任何值,组指针只会move,(手机上)如果设置了
    #define MOUSE_LEFT_KEY_MASK 0x1
    屏幕会被拖动。推测在电脑上,RIGHT_KEY也是会响应的。

参考资料:

一些相关博客
https://blog.csdn.net/sinat_37343534/article/details/116306013
https://m.xuejianbihua.com/item/xYTE3ZDUyODVhNzJjMTY4MDk4YzhmMmY2u.html
https://blog.csdn.net/sinat_37343534/article/details/116446980
控制台版本的sink端代码实现
https://github.com/albfan/miraclecast/tree/master/src/uibc
疑似华为手机source uibc的实现代码
https://github.com/SivanLiu/HwFrameWorkSource/blob/5b92ed0f1ccb4bafc0fdb08b6fc4d98447b754ad/Mate20_9_0_0/src/main/java/com/android/server/display/HwUibcReceiver.java
lg电视的实现效果
https://www.youtube.com/watch?v=Q-I9g9g_21g
苏州必捷的实现效果
https://zhuanlan.zhihu.com/p/437928388
这下面有client代码
~/code/giant-mtk/vendor/mediatek/proprietary_tv/open/hardware/wfd_client
https://www.usb.org/hid
usb的hid鼠标键盘报告描述符:
http://t.zoukankan.com/zongzi10010-p-10155333.html
https://www.usb.org/sites/default/files/hid1_11.pdf
Android鼠标源码研究(五)–输入事件处理
https://blog.51cto.com/u_15067266/2908866
Tutorial about USB HID Report Descriptors
https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/
HID鼠标描述符
https://www.luliang.vip/archives/4.html

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个简单的Android代码示例,演示如何在Miracast连接中使用Miracast UIBC传输用户输入: 首先,在AndroidManifest.xml文件中添加以下权限和特性: ```xml <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-feature android:name="android.hardware.wifi.direct" android:required="true" /> ``` 然后,在你的Activity中添加以下代码: ```java import android.content.Context; import android.media.MediaRouter; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyEvent; import android.view.MotionEvent; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private MediaRouter mMediaRouter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mMediaRouter = (MediaRouter) getSystemService(Context.MEDIA_ROUTER_SERVICE); } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { sendUIBCKeyEvent(event); } return super.dispatchKeyEvent(event); } @Override public boolean dispatchGenericMotionEvent(MotionEvent event) { sendUIBCMotionEvent(event); return super.dispatchGenericMotionEvent(event); } private void sendUIBCKeyEvent(KeyEvent event) { MediaRouter.RouteInfo selectedRoute = mMediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_VIDEO); if (selectedRoute != null && selectedRoute.isEnabled()) { InputEvent inputEvent = new KeyEvent(event.getDownTime(), event.getEventTime(), event.getAction(), event.getKeyCode(), event.getRepeatCount(), event.getMetaState(), event.getDeviceId(), event.getScanCode(), event.getFlags(), event.getSource()); selectedRoute.sendControlEvent(MediaRouter.ControlRequestCallback.NONE, inputEvent); } } private void sendUIBCMotionEvent(MotionEvent event) { MediaRouter.RouteInfo selectedRoute = mMediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_VIDEO); if (selectedRoute != null && selectedRoute.isEnabled()) { InputEvent inputEvent = MotionEvent.obtain(event); selectedRoute.sendControlEvent(MediaRouter.ControlRequestCallback.NONE, inputEvent); } } } ``` 这段代码中,我们首先在onCreate方法中获取MediaRouter实例。然后,在dispatchKeyEvent方法中检测按键事件,将其封装成UIBC的KeyEvent并通过MediaRouter发送到显示设备。同样地,在dispatchGenericMotionEvent方法中检测触摸事件,将其封装成UIBC的MotionEvent并发送到显示设备。 请注意,这只是一个简单的示例,实际使用时还需要根据你的具体需求进行适配和错误处理。 希望这个示例能帮到你!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值