第 10 章:输入后端——SDK、UHID 与 AOA 的三种世界
scrcpy 不把“键盘控制”绑定为一种实现。processor 接口先统一内部事件,再把注入策略延迟到运行时选择:语义事件、虚拟硬件或真实 USB accessory HID。
一、三个 processor 接口
客户端定义:
sc_key_processor:process_key必选,process_text可选,并声明async_paste、hid;sc_mouse_processor:motion/click 必选,scroll/touch 可选,并声明是否 relative mode;sc_gamepad_processor:设备增删、axis、button。
接口定义见 key_processor.h、mouse_processor.h 和 gamepad_processor.h。
input_manager 只持有这三个接口指针,所以快捷键、窗口坐标与 clipboard 逻辑可被所有后端复用。
二、三条路径对照
| 后端 | 数据路径 | 设备看到什么 | 依赖 | 典型优势 |
|---|---|---|---|---|
| SDK | control socket → InputManager | 注入的 Android InputEvent | ADB control | 文本注入、绝对触摸、配置简单 |
| UHID | control socket → /dev/uhid | 虚拟物理 HID 设备 | Android/Linux UHID | 键盘布局自然、相对鼠标、手柄 |
| AOA | libusb control transfer | USB accessory HID | 物理 USB + libusb | 输入绕过 Android server/control socket |
后端选择与实例装配集中在 scrcpy.c 591–751 行。
三、SDK:发送“动作语义”
keyboard_sdk 把宿主按键映射为 Android keycode/meta state,然后构造 INJECT_KEYCODE 或 INJECT_TEXT。mouse_sdk 构造 touch/scroll 控制消息。设备端再创建 KeyEvent/MotionEvent 调系统服务。
优点:
- 可指定 display id,适合多显示与虚拟显示;
- 可直接注入 UTF-8 文本;
- 支持绝对触摸、面板与系统动作;
- 不需要注册虚拟硬件。
限制:
- 键位解释受 Android keycode 与 KeyCharacterMap 影响;
- 某些设备要求额外“USB debugging (Security Settings)”;
- 鼠标更像触摸模拟,而非完整硬件鼠标。
SDK 键盘 ops 装配见 keyboard_sdk.c。
四、UHID:发送“硬件 report”
UHID 路径不发送 Android keycode,而发送三类控制消息:
UHID_CREATE:id、vendor/product、设备名、report descriptor;UHID_INPUT:一帧 HID input report;UHID_DESTROY:关闭虚拟设备。
设备端 UhidManager 打开 /dev/uhid,写 create2/input/destroy 事件,并监听 output report,见 UhidManager.open()。
Android 因而把输入看成真实连接的键盘、鼠标或手柄:键盘布局由系统物理键盘设置处理,相对鼠标能触发 pointer capture,游戏也能收到标准 gamepad。
output report 为什么要返回桌面
物理键盘会收到 LED 状态(Caps Lock 等)output report。设备端监听 UHID fd 后把 report 封成反向消息,客户端 uhid_devices 再更新本地 HID 状态。控制通道因此不仅“输入设备”,还模拟了硬件的双向反馈。
五、AOA:绕过 server 的 USB HID
Android Open Accessory 2.0 定义了 accessory 侧注册 HID 的控制请求。scrcpy 通过 libusb:
ACCESSORY_REGISTER_HID注册 id 与 descriptor 总长;- 可能分片发送
ACCESSORY_SET_HID_REPORT_DESC; ACCESSORY_SEND_HID_EVENT发送 report;- 关闭时 unregister。
请求常量和实现见 aoa_hid.c 与 sc_aoa_register_hid。
这条路径完全不经过 control socket 的输入发送,甚至可用于 scrcpy --otg 的无投屏纯控制模式。但设备必须通过 USB 物理连接,平台构建也要启用 libusb。
六、共享 HID 层避免三份状态机
app/src/hid 维护 report descriptor、按键 bitmap、modifier、鼠标轴/button、gamepad axis/button 等通用状态。UHID 与 AOA 只负责“如何发送 open/input/close”,不各自重新解释 SDL event。
例如 keyboard backend 最终都消费共享 sc_hid_input;改变一个按键时根据当前完整键盘状态生成 report。这样按键同时按下、repeat、release 和 LED 状态不会因 transport 不同出现分叉。
七、relative mouse 为何改变 UI 行为
UHID/AOA 鼠标通常是相对位移:report 只说 dx/dy,不带屏幕绝对位置。因此 mouse_processor.relative_mode=true,screen 必须捕获鼠标、隐藏宿主光标,并在窗口焦点/快捷键时正确释放。
SDK 鼠标则需要当前窗口坐标映射到视频坐标,属于绝对模式。一个接口字段就让 UI 根据后端切换交互模型,而不用判断具体类型。
八、HID paste 的同步难题
HID 没有“注入字符串”,Ctrl+V 是最可靠的多语言文本输入方式:
- 把电脑 clipboard 发给设备;
- 等设备 ACK;
- 再发送 HID Ctrl+V。
key_processor.async_paste 告诉 input manager 该后端必须等待 ACK。SDK 后端可在同一 control handler 内 set clipboard 后调用 PASTE,不需要外部等待。
UHID 键盘等待逻辑可见 keyboard_uhid.c,接口上的语义注释见 key_processor.h。
九、选型不是“越底层越好”
| 场景 | 推荐 | 原因 |
|---|---|---|
| 常规手机触控与文本 | SDK | 支持绝对位置、文本和显示 id |
| 桌面模式/游戏的真实鼠标 | UHID | 相对鼠标与硬件语义 |
| 游戏手柄 | UHID 或 AOA | 系统识别标准 gamepad |
| 只有 USB 控制、不需要镜像 | AOA | 不依赖 server 媒体链路 |
| 无线 ADB | SDK/UHID | AOA 需要物理 USB |
| 复杂非 ASCII 文本 | SDK clipboard/paste | HID 逐键受布局影响 |
十、扩展一个后端需要改哪里
一个新 input transport 通常需要:
- 实现相应 processor ops;
- 复用或扩展
hid状态机; - 在 options 增加枚举与校验;
- 在
scrcpy.c组合根创建具体对象并赋给kp/mp/gp; - 定义生命周期 start/stop/destroy;
- 若跨 control socket,双端扩展消息与测试。
input manager 原则上无需修改,除非新后端需要新的内部事件语义。
十一、本章小结
SDK、UHID、AOA 不是同一路径的三个参数,而是三个不同抽象层级:操作系统事件、虚拟硬件、USB 硬件。processor 接口让 UI 与这些差异隔离,共享 HID 层又避免状态机重复。下一章回到 screen,解释窗口中的一个坐标如何经过 letterbox、旋转、镜像和 session 尺寸后准确落到 Android display。