miracast 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;
  }
1
2
3
4
然后接收控制台输入事件

    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;
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
根据不同的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;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
4.3 发送带input body的uibc包
r = sendUibcMessage(&uibcmessage, sockfd);
if (r) {
      return r;
}
close(sockfd);
1
2
3
4
5
前面组完包之后就调用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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
到这一步,基本上,发送和接收都完成了一遍。只不过,接收是华为手机,为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);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
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;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
}
1
2
3
4
5
6
7
8
到这里就很全面了,过程推测应该和前面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());
    ...
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
终于知道这个eventDesc是怎么构造的了

0x30/0x31表示touch down/touch up, 0x33/0x34表示key down/key up
keyCode转换成ascii码,比如home键的keycode是3。ascii码是0x33。那这里就应该是0x0033
再补上一个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);
            ...
        }
    }    
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
这段代码主要是循环发送了一个鼠标事件,坐标值从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));
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
上面这些复杂的结构体看名字头都晕。可以看下定义

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;
    };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
到这里,理论上来说功能已经可以实现了。按照固定格式组好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实验结果
button位符号位字节没用,设置了不识别,直接在坐标位置赋值实际计算出来的正/负delta就可以。
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

文章知识点与官方知识档案匹配,可进一步学习相关知识
Java技能树首页概览118258 人正在系统学习中

bberdong
关注

2


6


1

专栏目录

1 条评论
m0_46476384
热评
你这个将13个字节的数据长度改成12,第十三个应该表示的是鼠标滚轮01 FF的数值吧,想问下如果倒数几位改成 04 00 00 00 01会不会模拟了鼠标滚轮从而实现反控手机端上下翻页
写评论
Miracast UIBC可选扩展功能介绍(二)
sinat_37343534的博客
 1528
前一篇文章已经说明,UIBC协议有两种类型:generic和HIDC。 目前Android设备一般支持generic,Win10支持HIDC协议。也有特例,比如荣耀的手机支持HIDC类型的UIBC。 HIDC存在的问题,在sink端发送鼠标事件的数据,荣耀手机可以正常响应,但是win10无法正常响应鼠标事件。 分析win10上的问题: Wireshark抓到的数据包都是正常的,win10接收到数据包,但是无响应。 注意事项:win10在刚一开始连接上miracast时,PC端必须点击允许鼠标.
Wi-Fi_Display_Technical_Specification_v2.1_0文档中UIBC可选扩展功能介绍(一)
sinat_37343534的博客
 1282
Miracast中的UIBC 反向控制 以下部分参照英文文档翻译而来: 4.11用户输入反向控制通道 用户输入反向控制通道(UIBC)是一个可选的WFD特性,实现该扩展功能,有助于用户从WFD sink端控制WFD source端的通信。 用户所有的UIBC输入都使用一个公共数据包头进行打包,并通过TCP/IP传输。反向控制的类型有Generic和HIDC。 Generic类型的输入用于在应用程序级别处理的与设备无关的用户输入。generic输入的信息使用generic 数...
Ubuntu下Python的安装及管理
最新发布
虎哥LoveDroid
 465
Ubuntu下python的安装及配置。
[WFD][UIBC]连接Wifi display后,有时会自动启动物理键盘
sandform的博客
 2429
[WFD][UIBC]连接Wifi display后,有时会自动启动物理键盘 [DESCRIPTION] 目前KK版本支持WIFI Display的load有时连接Wifi display dongle后,手机会自动启动物理键盘,进入任何编辑界面 ,只有物理键盘无法调出软键输入法。 如下图是没有连接无线显示的语言和输入法画面: 4.27tp-18.png (105.83
miracast 源码
12-17
miracast 原生代码,miracast 原生代码miracast 原生代码miracast 原生代码
Miracast技术详解(二):RTSP协议
Venus 的博客
 1106
在上一篇博客中我们已经通过Wi-Fi P2P建立好了Source和Sink端的TCP连接,在Miracast后续的音视频传输过程中,将采用RTSP协议来对流媒体进行控制。因此接下来的步骤就到了RTSP协商、会话建立及流媒体传输的阶段。首先,什么是RTSP协议呢?实时流协议是一种网络应用协议,专为娱乐和通信系统的使用,以控制流媒体服务器。该协议用于创建和控制终端之间的媒体会话。媒体服务器的客户端发布VCR命令,例如播放,录制和暂停,以便于实时控制从服务器到客户端(视频点播)或从客户端到服务器(语音录音)的媒体
无线高清会议室终端之Miracast
wanghorse的专栏
 4596
前言:      公司在2015年5月立项做一款硬件类产品,使用SOC(CPU+DSP)芯片方案做一款高清会议室终端,除了会议功能之外,需要具备多路无线投屏功能。项目从5月份立项、历经搭建团队、研发、生产、测试,至2016年1月完成一期功能和试产,期间经历很多困难和曲折,现分一个系列做下回顾,本篇讲下Miracast。 Miracast:      关于Miracast的基础知识,大家可
miraclecast:通过Wifi-Display规范(也称为Miracast)将外部显示器连接到系统
03-11
MiracleCast-Wifi显示器/ Miracast实施 MiracleCast项目提供了用于通过Wi-Fi将外部显示器连接到系统的软件。 它与也称为Miracast的Wifi-Display规范兼容。 MiracleCast实现了Display-Source和Display-Sink端。 通过“显示源”侧,您可以将外部显示器连接到系统,并将本地内容流式传输到设备。 与通过HDMI连接外部显示器一样简单,这需要付出很多努力。 另一方面,“显示接收器”侧允许您自己创建具有wifi功能的外部显示器。 您可以在嵌入式设备甚至完整的台式机上使用它,以允许其他系统将您的设备用作外部显示器。 要求 MiracleCast项目需要安装以下软件: systemd :系统管理守护程序。 它用于设备管理(udev),dbus管理(sd-bus)和服务管理。 Systemd> = 221将立即可用
mircast工程的编译
yd4751的专栏
 2169
工作杂记
【转载】多屏互动技术研究之WifiDisplay(Miracast)技术原理及实现
分享技术与经验的博客
 3962
文章目录 WifiDisplay(Miracast)技术原理及实现1. WifiDisplay简介2. WifiDisplay协议流程3. WifiDisplay显示框架实现4. Android WifiDisplay实现4.1 Source端实现4.1.1 设备扫描及发现 4.2 Sink端的实现4.2.1 设备如何被发现4.2.2 设备p2p连接后如何建立RTSP连接4.2.3 RTSP连接实现 1. WifiDisplay简介 Wi-Fi Display经常和M...
Android反向控制基本Demo
04-09
Android反向控制基本Demo
Android WIFI实例
09-20
本例子主要展示了android里wifi的基本用法,包括开启、关闭、搜索、连接等等,供大家参考
WifiDisplay(Miracast)技术原理及实现
一个Android菜鸟的博客
 6351
WifiDisplay(Miracast)技术原理及实现 文章目录WifiDisplay(Miracast)技术原理及实现1. WifiDisplay简介2. WifiDisplay协议流程3. WifiDisplay显示框架实现4. Android WifiDisplay实现4.1 Source端实现4.1.1 设备扫描及发现4.2 Sink端的实现4.2.1 设备如何被发现4.2.2 设备p2p连接后如何建立RTSP连接4.2.3 RTSP连接实现 1. WifiDisplay简介 Wi-Fi Disp
Miracast投屏反控
faststream的博客
 5196
Miracast介绍 Miracast是对支持Wi-FiDisplay功能设备的认证名称,也就是通过Miracast认证的设备应该都支持Wi-FiDisplay功能。 缩略词以及定义 Source端:支持通过WiFi链路将多媒体内容流式输入到接收端的设备,即发送端。 Sink 端:从source端通过WiFi链路接收多媒体内容并进行渲染的设备,即接收端。 WFD : Wi-FiDisplay UIBC: User Input Back Channel 用户输入反向通道,将用户输入的操作传输到...
miracast移植记录
qq_42733114的博客
 230
全志Miracast source移植 Date:2021-12-7 基于V536 + AP6255实现Miracast source功能移植及实现 1. 环境配置 1.1 V536环境配置 在sdk目录下source build/envsetup.sh 输入lunch 选择需要的响应的板型 1.2 菜单配置 在sdk根目录执行make menuconfig,依照下面的配置依次配置 >make menuconfig Global build settings ---...
Miracast技术详解(四):Sink源码解析
Venus 的博客
 1503
Miracast Sink端源码最早出现在上,通过可以很方便的查看: https://android.googlesource.com/platform/frameworks/av/+/android-4.2.2_r1.2/media/libstagefright/wifi-display/sink/ 但是在以后,Google却移除掉了这部分源码,详细的commit记录在: h
————————————————
版权声明:本文为CSDN博主「bberdong」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/bberdong/article/details/126423909

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值