多媒体 哔哩哔哩技术
2024年09月03日 12:02 上海
一、背景
PC直播姬中的直播素材之一——投屏源可与安卓或iOS移动端应用(直播姬、粉版)配合使用,将移动端画面投射到PC直播姬中。投屏源最初仅支持无线投屏,即通过局域网 WiFi传输,但这样的链路会受到网络质量影响,而且如果Windows计算机和移动设备不在同一网段或者配置了局域网隔离,那么就无法投屏成功。
无线投屏的这些缺点,使用USB有线投屏即可克服。本文基于Windows平台,介绍计算机与安卓/iOS通过USB交换数据的实现方式。
二、预备知识
在讲述具体的内容之前,有必要先了解一下USB设备相关的一些基础知识。USB(通用串行总线)是一种用于设备与设备之间互连的串行总线标准 [1]。其总线拓扑如下图所示:
图2-1 USB总线拓扑
图2-1中,集线器和功能设备统称为USB设备(为简单起见,下文将使用“USB设备”来称呼功能设备)。一个USB系统中只有一个主机,主机通过主机控制器(Host Controller)来与根集线器交互。
每个USB设备都被赋予了两个数字标识:
Vendor ID:生产该设备的制造商的标识Product ID:设备某一型号的标识,相同型号的设备具有相同的Product ID在Windows平台上,USB设备和设备驱动之间通过Vendor ID和Product ID关联。新安装的USB设备驱动会取代已安装的具有相同Vendor ID和Product ID的驱动(如果有的话)。在使用有线投屏时,就需要安装对应于用户使用的移动设备USB驱动,来取代其原有的设备驱动。这一点在下文将详细叙述。
三、iOS有线投屏
首先介绍iOS有线投屏,整个投屏链路组成如图3-1所示。下文将分别讲述Windows端和iOS端的实现细节。其中,Windows计算机上的接收端软件作为连接的发起方,iOS设备上的应用程序作为接受方。
图3-1 iOS有线投屏链路组成
3.1 Windows端实现
相比安卓,iOS有线投屏Windows端的实现较为简便。其核心是libimobiledevice开源库(LGPL2.1协议) [2]以及苹果的iTunes桌面端软件附带的AppleMobileDeviceSupport服务程序(下文简称苹果服务) [3]。
3.1.1 苹果服务
需要在Windows计算机上安装该程序,该程序将会创建一个Windows服务,名称为Apple Mobile Device Service,负责与苹果驱动通信。该服务是libimobiledevice与iOS设备通信的桥梁。
iTunes也使用该服务程序,因此如果已经安装了iTunes桌面端,则无需再安装服务程序。
苹果服务安装成功后,外部程序即可使用libimobiledevice与通过USB线缆连接到Windows计算机的iOS设备通信了。
3.1.2 枚举/连接设备
在第一次连接iOS设备时,Windows计算机会为其安装合适的驱动程序。设备管理器中会显示详细的设备信息。
图3-2 iOS设备连接后设备管理器中的情况
正常情况下,连接iOS设备(这里使用了iPad),驱动安装完成后,设备管理器中相关的设备节点如图3-2所示。其中:
“Apple iPad”是WPD(Windows Portable Devices)[4]设备,用于访问iPad的存储空间;“Apple Mobile Device USB Device”用于暴露数据通信端点,投屏就是通过该设备进行的。我们不会直接和该设备交互,而是通过苹果服务进行访问;“Apple Mobile Device USB Composite Device”作为前两个设备的父设备,实际是一个集线器。如果因为种种原因,导致这些驱动被其他具有相同Vendor ID和Product ID的驱动替换掉的话,投屏链路就无法建立。此时,iTunes也是处于无法和设备通信的状态。出现这种问题时,只能手动卸载掉这些驱动,然后重新连接设备,让Windows计算机重新安装官方驱动。
设备连接逻辑本身较为简单,如下图所示:
检查苹果服务是否已安装,使用libimobiledevice提供的API:idevice_get_device_list() ,查看其返回值,如果返回错误则表明服务未安装;第1步的API若返回成功,则得到当前连接在Windows计算机上的iOS设备列表;挑选设备之后,使用API:idevice_connect() 进行连接,调用时需要指定一个事先协商好的端口号。iOS设备上的应用程序将来需要使用这个端口号创建一个本地套接字,作为服务端。3.1.3 数据传输
投屏的数据链路建立之后,Windows计算机上的接收端程序即可调用API:idevice_connection_send() 和idevice_connection_receive_timeout() 来发送和接收数据。收发数据的调用是同步的。
如果在数据传输过程中断开USB电气连接,当前或后续的读写调用会立即返回错误。在此之后需要重新枚举设备。稳定连接速度实测为 35MiB/s 左右,使用USB2.0或者USB3.0接口,速度没有太大的差别。
通信时数据包使用如下头部:
struct TransferFrame { uint32_t version; uint32_t type; uint32_t tag; uint32_t payload_size; uint32_t identifier;};首先进行握手,握手数据包使用上述结构,没有载荷,流程见图3-3。握手成功之后iOS移动端将音视频数据编码后封装为FLV流,数据包加上上述头部后发送给Windows设备上的接收端程序,接收端将数据中的有效载荷送入解封装/解码器,将结果合并进入直播场景中。
图3-3 iOS有线投屏接收端逻辑流程图
图3-4 iOS有线投屏连接后载荷传递时序图
3.2 iOS端实现
iOS端的首要任务就是通过ReplayKit采集屏幕、麦克风和设备音频。随后使用Audio/VideoToolBox将音视频数据编码,并封装为FLV。最后通过基于usbmux协议的设备间通信,将FLV数据发给Windows计算机上的接收端程序。
3.2.1 屏幕录制
得益于苹果对iOS录屏框架replaykit的不断完善,在iOS 12时提供了稳定的API, 可以实现对iOS设备屏幕、麦克风和设备播放音频的录制。主要流程如下:
1)创建Broadcast Upload Extension,Xcode会新增Extension工程,选择Target运行。如图3-5所示:
图3-5 iOS录制工程
2)通过RPSystemBroadcastPickerView启动屏幕录制页面,或者设置→控制中心→添加屏幕录制,在控制面板长按屏幕录制按钮启动页面:
图3-6 iOS屏幕录制页面
3)创建Broadcast Upload Extension后,会自动生成sampleHander类,当启动屏幕录制后,会触发processSampleBuffer:withType方法,回调实时采集的音视频数据:
图3-7 iOS录制回调
3.2.2 设备间通信简介
usbmux是苹果的私有协议,苹果设计该协议的原因是为了自家的macOS APP能够和iDevice进行通信,从而实现诸如iTunes备份iPhone、Xcode真机调试等功能。该协议提供了一种类似TCP socket的API,使得macOS和iOS设备之间的通信,如同是网络上的两个主机之间的通信。
Windows计算机则可以安装AppleMobileDeviceSupport服务, 与iOS设备之间建立通信。在第一次连接iOS设备时,Windows计算机会为其安装合适的驱动程序,设备管理器中会显示详细的设备信息,可参见图3-2。
3.2.3 建立连接
基于usbmux协议,iOS端启用应用后,创建Socket,监听协商的端口号,启动TCP Server。PC端则充当Client的角色,根据协商的端口号,调用libimobiledevice库连接iOS设备。
由于iOS启动屏幕录制后的Extension是与宿主APP相独立的一个进程,Extension与宿主APP的数据交互,同样的可以基于TCP Client/Server的机制,Extension内创建TCP Client连接宿主APP的TCP Server,进行数据的传输。传输数据所使用的结构参见3.1.4小节。
3.2.4 录制FLV数据封装
通过Replay Extension采集到屏幕的视频帧画面,其分辨率与设备屏幕尺寸等比例,可经过渲染对视频帧画面进行处理,如:裁剪,缩放,再使用VideoToolBox进行H.264编码,得到编码后的数据包。
采集的音频数据包含麦克风的声音和设备播放的音频,可以音频数据进行混音操作,合成单路音频流,然后使用AudioToolBox进行AAC编码。最后将编码后的音视频数据封装成FLV Packet。Packet经由TCP Server中转,发送至Windows计算机上的接收端程序。
图3-8 iOS录制数据封装流程
以上就是iOS有线投屏的全部内容。接下来是安卓有线投屏。
四、安卓有线投屏
安卓有线投屏目前存在两类方案: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]使iOS设备进入配件模式。
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() 筛选设备,主要参数填写如下:
ClassGuid填写为WPD分类的GUID{eec5ad98-8080-425f-922a-dabf3de3f69a} 以及我们自己定义的GUID(用于筛选出已经安装过我们的驱动的设备)Enumerator 填写为”USB”字符串。我们仅关心USB设备。调用成功后,即可使用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小节所述,若一个安卓应用想要用于配件模式通信,它声明的信息就必须与配件提供的信息对应。具体来说,安卓应用需要:
在清单文件中声明 <intent-filter> 和 <meta-data> 为hardware.usb.action.USB_ACCESSORY_ATTACHED,当配件连接时,应用将会收到通知;在安卓应用资源目录下声明关心的配件清单 res/xml/accessory_filter.xml,包含:型号、制造商和版本。这三个字段需要和Windows计算机上的接收端程序发送的字段一致(见表4-1)。4.2.3 连接配件
当有配件连接安卓设备,并使安卓设备进入配件模式,且安卓设备上安装有匹配配件信息的安卓应用时,应用的响应流程如图4-7所示:
图4-7 安卓设备对配件模式的响应
应用未启动时:当USB连接时,系统识别到清单文件中声明的配件信息,在用户同意后将唤醒应用。可通过安卓服务获取到USBManager来枚举出连接的配件,在校验通过后打开此配件将得到一个文件描述符。可以通过IO流对该文件描述符进行读写操作,即可完成传输。应用已启动时:可通过安卓BroadcastReceiver对USB连接进行监听,当发现有新的设备连接后,向系统服务USBManager,主动发起授权申请,当得到用户的授权后,系统将以广播的形式通知到应用,此时对配件权限进行校验后,打开文件描述符进行读写。4.2.4 录制FLV数据封装
数据输出流程包括采集、编码和封装三个主要阶段,最终封装为FLV格式的数据。投屏中使用MediaProjection进行屏幕采集,AudioRecord进行麦克风采集。通过MediaCodec对视频和音频分别进行编码,生成AVC视频流和AAC音频流。
设备连接时,通过UsbManager申请UsbAccessory操作权限,并获取ParcelFileDescriptor以实现IO输出。随后,我们将AVC和AAC数据队列按时序交替封装成FLV Packet,形成连续的数据流,并通过此IO输出传输至Windows端。
五、总结
搭建iOS有线投屏链路需要:
在用户的Windows计算机上安装苹果服务;软件中接入libimobiledevice库以进行设备枚举和数据通信。搭建安卓有线投屏链路需要:
准备好libusbK驱动模板和驱动生成逻辑,跟随软件部署;软件中调用SetupAPI枚举设备,接入libusb以进行数据通信;相对无线局域网来说,有线投屏连接更为稳定,仅需一根质量尚可的USB数据线缆,即使是USB2.0的带宽也可满足目前要求。但其也有缺点:数据链路的搭建逻辑较为复杂;需要安装额外的USB驱动程序;有线连接本身可能就会造成一些阻碍。但对于需要稳定投屏,或者电脑和手机无法通过局域网连接的用户来说,有线投屏可能是唯一的选择。
参考
[1]
USB-IF, "Universal Serial Bus 2.0 Specification," 2000.
[2]
libimobiledevice, "libimobiledevice," [Online]. Available: https://libimobiledevice.org.
[3]
Apple, "iTunes - Apple," [Online]. Available: https://www.apple.com/itunes.
[4]
Microsoft, "Windows Portable Devices," [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/windows-portable-devices.
[5]
libusb, "Windows," [Online]. Available: https://github.com/libusb/libusb/wiki/Windows#How_to_use_libusb_on_Windows.
[6]
T. L. Robinson, "libusbK," [Online]. Available: https://sourceforge.net/projects/libusbk.
[7]
Google, "Android 开放配件协议 1.0," [Online]. Available: https://source.android.com/docs/core/interaction/accessories/aoa?hl=zh-cn.
-End-
作者丨ucclkp、大鸡排、青藤