• 作者:老汪软件技巧
  • 发表时间:2024-09-05 07:01
  • 浏览量:

四、安卓有线投屏

安卓有线投屏目前存在两类方案:ADB方案和配件方案。使用ADB方案进行数据透传需要打开手机“开发者选项”中的“USB调试”功能,对于普通用户而言不是一个好选择,操作复杂且存在安全风险,不适合用于线上场景;配件方案对于绝大多数机型来说都不需要开启USB调试,且不需要额外权限,但需要启动App。本文选择配件方案。

表4-1 安卓有线投屏技术选型

安卓有线投屏的链路组成如图4-1所示。下文将分别讲述Windows计算机和安卓端的实现细节。其中,Windows计算机上的接收端软件作为连接的发起方,iOS设备上的应用程序作为接受方。

图4-1 安卓有线投屏链路组成

4.1 Windows端实现

安卓有线投屏Windows端的实现稍显复杂。数据链路使用libusb开源库(LGPL2.1协议)[5]和libusbK驱动[6]建立。连接时首先定位到目标设备,然后按照谷歌的安卓开放配件协议(AOA)1.0[7]使安卓设备进入配件模式。

4.1.1 枚举设备

虽然libusb提供了枚举设备的接口,但其提供的信息较少,即使是要获取设备名,也需要先打开设备。而很少有设备可以在不安装libusb相关驱动(比如libusbK)的情况下被libusb打开。于是这里直接调用Windows计算机提供的SetupAPI来获取设备信息。libusb在Windows平台上也是通过SetupAPI来获取USB设备信息的,但可能是出于跨平台考虑,许多信息没有通过接口暴露出来,而是供其内部使用。

使用SetupAPI可以拿到很多信息,例如设备描述、设备实例ID、设备类、父类、子类等等。即使有这些信息,要精确地仅枚举安卓设备仍很困难,目前采用的方法是枚举WPD(Windows Portable Devices)设备。该类设备在设备管理器中被列为“便携设备”。

图4-2 Windows设备管理器中的便携设备

虽然U盘或移动硬盘这种存储设备也被列为便携设备,但这些设备的设备实例路径前缀并非USB,通过SetupAPI就可以很容易地过滤。苹果设备可以通过Vendor ID过滤,苹果公司的Vendor ID是固定的(0x05AC)。

目前的过滤方案可以过滤大部分的非安卓设备,但可能仍有一些设备会逃过过滤。这些设备并非安卓,并且存在Windows计算机可访问的存储空间,但又不是U盘、移动硬盘这种纯粹的存储设备,比较有可能的是数码相机。在下一节可以看到,我们需要为选择的设备安装libusbK驱动,然后才能和设备通信,如果选择了错误的设备,安装的驱动会把原有驱动替换掉,在此之后使用当前的PC是无法访问设备的存储空间的。要再次访问设备存储,需要卸载libusbK驱动,并重新连接USB线缆。

此外,可能一些设备已经在之前建立过连接,我们已经为其装过驱动,这些设备也应该列入候选列表中。在下一节可以看到,我们安装的驱动有特殊的类GUID,可以很容易地过滤。

实际代码中,首先使用Windows设备管理API:SetupDiGetClassDevsW() 筛选设备,主要参数填写如下:

调用成功后,即可使用SetupDiEnumDeviceInfo() 进行实际的枚举操作。我们感兴趣的信息是设备实例ID,可唯一标识连接到系统的某个USB设备。该ID可通过SetupDiGetDeviceInstanceIdW() 获得,ID字符串形如USB\VID_1234&PID_5678…。虽然微软文档不建议对该字符串进行解析,但没有其他更合适的方法拿Vendor ID 和Product ID了。

除此之外,还需要调用SetupDiGetDevicePropertyW() 获取以下信息:

还有一点需要说明的是,为了后续libusb能够正常工作,我们需要确保获取到的设备是具有同一Vendor ID 和Product ID的父根设备。可使用Windows API:CM_Get_Parent() 和CM_Get_Device_IDW() 不断向上遍历,直到再向上Vendor ID 和Product ID不同的时候再停止,此时的设备就是我们需要的设备。新版libusb似乎已经可以支持子设备,但目前并未验证。

4.1.2 连接设备

当我们选定了要连接的设备,同时就得到了设备的Vendor ID 和Product ID。除此之外还有设备实例ID,让我们能够区分连接到同一台PC的多台同一型号的安卓设备,这些设备具有相同的Vendor ID和Product ID,但Windows计算机为这些设备分配的设备实例ID是不同的。

由于我们后续的逻辑基于libusb,因此需要使用上一步拿到的信息获取libusb的设备对象。先使用libusb_get_device_list() 获取USB设备列表进行遍历,然后使用libusb_get_device_descriptor() 获取设备的描述信息,和上面拿到的信息进行比对,来找到目标设备。

详细的连接逻辑如下图所示:

图4-3安卓有线投屏接收端发起连接逻辑

连接流程可以概括为:

为选择的设备安装libusbK驱动(如果之前没有安装过),驱动安装完毕后,即可使用libusb访问设备。关于驱动如何部署,见下文;根据谷歌的AOAv1文档,向设备发送指令,将设备切换至配件模式。切换后安卓系统会弹出相应的提示。见下文;如果设备成功切换,则其报告的Vendor ID和Product ID会发生变化,从原先的对应设备制造商的ID转换为谷歌预留的固定ID:0x18D1和0x2D00,如果设备开启了USB调试,则转换后的ID是0x18D1和0x2D01。可由此判断设备是否开启了USB调试;因为设备的Vendor ID和Product ID变化,需要再次安装libusbK(如果之前没有安装过),驱动安装完毕后,即可使用libusb访问处于配件模式的设备,连接建立完成。

上述流程中,第一次安装的驱动需要和想要连接的设备的Vendor ID和Product ID对应。这个驱动将会在选择设备后,由一个事先准备好的驱动模板填入Vendor ID和Product ID生成。驱动模板使用libusbK附带的驱动开发包 (UsbK Development Kit) 中的驱动安装包创建向导 (Driver Install Creator Wizard) 基于任一USB设备生成的驱动安装包修改而成。该流程对于第二次安装的驱动来说也是一样的。

驱动安装包文件中的二进制文件可以直接使用,而且驱动文件已经打过签名。INF文件是我们修改的目标,里面记录了对应设备的Vendor ID和Product ID,显示名称、制造商名称、类GUID等等。类GUID可以改为我们独有的GUID,以便于在设备枚举阶段筛选出已经装过驱动的设备。

INF文件修改完成后,执行同一目录中的dpinst64.exe文件即可安装驱动。需要注意的是,该可执行文件需要管理员权限,执行前最好进行文件校验,以防篡改。

接下来说明配件模式相关的数据。在步骤2中,发送的指令可携带以下信息:

表4-2安卓配件信息

发送时序如图4-4所示:

图4-4安卓有线投屏切换配件模式时序图

首先使用libusb_open() 打开设备,并使用libusb_claim_interface() 打开读写接口,然后就可以使用libusb_control_transfer() 发送和接收数据了,具体指令格式可参阅谷歌AOAv1文档,这些信息用于匹配安卓端的应用程序。切换完成之后,需要重新枚举设备,找到固定ID:0x18D1和0x2D00的设备,使用libusb_open() 打开设备,并使用libusb_claim_interface() 打开读写接口进行后续数据通信。

如果安卓端安装有对应的应用程序,转换为配件模式后,安卓端会弹出如图4-5左侧的系统消息;如果没有对应的应用,则安卓端会弹出如图4-5右侧的系统消息。图中红框标出的部分对应“配件描述”。

图4-5安卓配件提示

安卓端用于处理配件模式的应用需要做出声明,参见4.2.2小节。

如果安装有对应的应用,点击“确定”按钮将拉起该应用;如果安装有多个对应的应用,则会弹出应用选择弹窗,选择其中一个应用后会将其拉起。在该应用中使用安卓Framework层提供的USBManager可打开对应的附件,得到文件描述符,可对该文件描述符进行读写操作。到此通信链路即建立完成。

4.1.3 数据传输

_安卓投屏显示器_安卓投屏线使用说明

投屏的数据链路建立之后,Windows计算机可以调用libusb API:libusb_bulk_transfer() 来进行设备通信。建议设置1秒超时,以防止出现无限等待的情况。

如果在数据传输过程中断开USB电气连接,当前或后续的读写调用会立即返回错误。在此之后需要重新枚举设备。稳定连接速度实测为 30MiB/s 左右。使用USB2.0或者USB3.0接口,速度没有太大的差别。

通信时数据包使用的头部与iOS有线投屏相同。握手和后续的接收数据流程也与iOS有线投屏基本相同,参考流程图3-3即可。

4.1.4 驱动卸载

在前面的连接步骤中,我们为选定的设备安装了libusbK驱动,这一过程会替换掉设备原本的驱动,换言之,设备将失去原本驱动所能提供的功能。例如:Windows计算机访问安卓的文件系统,或者Windows计算机通过安卓连接网络。因此,有必要提供卸载功能,使得用户在不使用投屏时,仍可以使用原驱动的功能。

驱动卸载分为以下两步:

1)移除设备

使用Windows API:SetupDiGetClassDevsW(),传入我们定义的设备类GUID,以及枚举器”USB”。然后使用SetupDiEnumDeviceInfo() 进行枚举,保存枚举到的设备的以下信息:

然后调用SetupDiCallClassInstaller(),传入DIF_REMOVE进行设备移除。

2)卸载驱动软件

以上一步获得的INF文件名,调用SetupUninstallOEMInfW(),删除驱动。

4.2 安卓端实现

在安卓端,对音视频编码后,通过UsbManager获取配件文件描述符,进行读写即可完成数据传输至另一端。其中,应用层程序通过安卓Framework层获得UsbManager代理对象,经过授权后拿到FileDescriptor,此时Framework 层会通过UsbDeviceManager打开"/dev/usb_accessory"并获取必要的描述信息,以供附件匹配,在确认匹配和授权后,应用层即可进行IO操作。

4.2.1 设备模式说明

安卓从3.1版本开始支持USB配件和主机两种模式,两种模式的示意见图4-6。安卓设备作为USB主机时,可连接U盘等USB设备,并由安卓设备提供电力;而在配件模式时,配件作为USB主机连接至安卓设备,并为安卓设备供电。在有线投屏场景中,Windows计算机就是“配件”。

图4-6 安卓设备USB主机/配件模式(图片来源:Google官网)

4.2.2 启用配件

如4.1.2小节所述,若一个安卓应用想要用于配件模式通信,它声明的信息就必须与配件提供的信息对应。具体来说,安卓应用需要:

在清单文件中声明 和 为hardware.usb.action.USB_ACCESSORY_ATTACHED,当配件连接时,应用将会收到通知;

在安卓应用资源目录下声明关心的配件清单 res/xml/accessory_filter.xml,包含:型号、制造商和版本。这三个字段需要和Windows计算机上的接收端程序发送的字段一致(见表4-1)。

4.2.3 连接配件

当有配件连接安卓设备,并使安卓设备进入配件模式,且安卓设备上安装有匹配配件信息的安卓应用时,应用的响应流程如图4-7所示:

图4-7安卓设备对配件模式的响应

4.2.4 录制FLV数据封装

数据输出流程包括采集、编码和封装三个主要阶段,最终封装为FLV格式的数据。投屏中使用MediaProjection进行屏幕采集,AudioRecord进行麦克风采集。通过MediaCodec对视频和音频分别进行编码,生成AVC视频流和AAC音频流。

设备连接时,通过UsbManager申请UsbAccessory操作权限,并获取ParcelFileDescriptor以实现IO输出。随后,我们将AVC和AAC数据队列按时序交替封装成FLV Packet,形成连续的数据流,并通过此IO输出传输至Windows端。

五、总结

搭建iOS有线投屏链路需要:

搭建安卓有线投屏链路需要:

相对无线局域网来说,有线投屏连接更为稳定,仅需一根质量尚可的USB数据线缆,即使是USB2.0的带宽也可满足目前要求。但其也有缺点:数据链路的搭建逻辑较为复杂;需要安装额外的USB驱动程序;有线连接本身可能就会造成一些阻碍。但对于需要稳定投屏,或者电脑和手机无法通过局域网连接的用户来说,有线投屏可能是唯一的选择。

参考[1]USB-IF, "Universal Serial Bus 2.0 Specification," 2000.

[2]

libimobiledevice, "libimobiledevice," [Online]. Available:.

[3]

Apple, "iTunes - Apple," [Online]. Available:

[4]

Microsoft, "Windows Portable Devices," [Online]. Available:/en-us/windo….

[5]

libusb, "Windows," [Online]. Available:.

[6]

T. L. Robinson, "libusbK," [Online]. Available:/projects/li….

[7]

Google, "Android 开放配件协议 1.0," [Online]. Available:/docs/core/i….