# 输入设备控制 **本文引用的文件** - [inputremoting.js](file://client/src/inputremoting.js) - [inputdevice.js](file://client/src/inputdevice.js) - [sender.js](file://client/src/sender.js) - [gamepadhandler.js](file://client/src/gamepadhandler.js) - [pointercorrect.js](file://client/src/pointercorrect.js) - [memoryhelper.js](file://client/src/memoryhelper.js) - [keymap.js](file://client/src/keymap.js) - [mousebutton.js](file://client/src/mousebutton.js) - [touchflags.js](file://client/src/touchflags.js) - [touchphase.js](file://client/src/touchphase.js) - [charnumber.js](file://client/src/charnumber.js) - [gamepadbutton.js](file://client/src/gamepadbutton.js) - [renderstreaming.js](file://client/src/renderstreaming.js) - [peer.js](file://client/src/peer.js) - [signaling.js](file://client/src/signaling.js) - [logger.js](file://client/src/logger.js) ## 目录 1. [简介](#简介) 2. [项目结构](#项目结构) 3. [核心组件](#核心组件) 4. [架构总览](#架构总览) 5. [详细组件分析](#详细组件分析) 6. [依赖关系分析](#依赖关系分析) 7. [性能考量](#性能考量) 8. [故障排查指南](#故障排查指南) 9. [结论](#结论) 10. [附录](#附录) ## 简介 本技术文档围绕“输入设备远程控制”能力,系统性阐述客户端侧输入设备抽象层与事件转发机制。文档覆盖鼠标、键盘、触摸屏、游戏手柄四类输入的事件捕获、状态转换与序列化打包;解释统一的输入事件与状态事件格式、时间戳与坐标系映射策略;并给出跨设备输入同步的关键点(时间戳、事件队列与延迟补偿思路)。文中所有技术细节均基于仓库现有源码进行归纳总结。 ## 项目结构 客户端输入子系统位于 client/src 目录,主要由以下模块组成: - 输入设备抽象与事件模型:inputdevice.js - 输入采集与事件分发:sender.js - 远程转发与消息封装:inputremoting.js - 游戏手柄轮询与事件:gamepadhandler.js - 坐标映射与视频适配:pointercorrect.js - 内存位操作工具:memoryhelper.js - 键盘键位映射与字符编码:keymap.js、charnumber.js - 按钮位定义:mousebutton.js、gamepadbutton.js - 触摸状态与阶段:touchphase.js、touchflags.js - WebRTC 信令与数据通道:renderstreaming.js、peer.js、signaling.js - 日志工具:logger.js ```mermaid graph TB subgraph "输入采集层" S["Sender
事件采集与分发"] GP["GamepadHandler
手柄轮询"] PC["PointerCorrector
坐标映射"] end subgraph "输入抽象层" ID["InputDevice/IInputState
设备与状态模型"] IE["InputEvent/StateEvent/TextEvent
事件与状态封装"] end subgraph "远程转发层" IR["InputRemoting
事件订阅与发送"] MSG["Message/NewEventsMsg/NewDeviceMsg
消息封装"] end subgraph "传输层" RS["RenderStreaming
信令与Peer管理"] PEER["Peer
RTCPeerConnection封装"] SIG["Signaling/WebSocketSignaling
信令通道"] end S --> ID S --> PC S --> GP ID --> IE IR --> MSG MSG --> RS RS --> PEER RS --> SIG ``` 图表来源 - [sender.js:14-188](file://client/src/sender.js#L14-L188) - [inputdevice.js:40-719](file://client/src/inputdevice.js#L40-L719) - [inputremoting.js:63-300](file://client/src/inputremoting.js#L63-L300) - [renderstreaming.js:11-317](file://client/src/renderstreaming.js#L11-L317) - [peer.js:3-188](file://client/src/peer.js#L3-L188) - [signaling.js:3-292](file://client/src/signaling.js#L3-L292) 章节来源 - [sender.js:14-188](file://client/src/sender.js#L14-L188) - [inputdevice.js:40-719](file://client/src/inputdevice.js#L40-L719) - [inputremoting.js:63-300](file://client/src/inputremoting.js#L63-L300) - [renderstreaming.js:11-317](file://client/src/renderstreaming.js#L11-L317) - [peer.js:3-188](file://client/src/peer.js#L3-L188) - [signaling.js:3-292](file://client/src/signaling.js#L3-L292) ## 核心组件 - 输入设备抽象层 - 设备基类与具体设备:InputDevice、Mouse、Keyboard、Touchscreen、Gamepad - 状态抽象与序列化:IInputState 及其派生 MouseState、KeyboardState、TouchscreenState、GamepadState - 事件模型:InputEvent、StateEvent、TextEvent - 输入采集与分发 - Sender:注册各类 DOM 事件,调用设备 queueEvent,生成 StateEvent/TextEvent 并通过自定义事件广播 - PointerCorrector:将页面坐标映射到视频区域坐标,适配黑边(Letterbox) - 远程转发与消息封装 - InputRemoting:订阅本地事件,打包为 Message,通过观察者(如 RTC 数据通道)发送 - Message/NewEventsMsg/NewDeviceMsg:二进制消息结构与序列化 - 手柄支持 - GamepadHandler:周期扫描并派发 gamepadupdated 事件 - 工具与常量 - MemoryHelper:按位写入布尔状态 - Keymap/CharNumber:键盘键位与字符编码映射 - MouseButton/GamepadButton:按钮位定义 - TouchPhase/TouchFlags:触摸阶段与标志位 章节来源 - [inputdevice.js:40-719](file://client/src/inputdevice.js#L40-L719) - [sender.js:14-188](file://client/src/sender.js#L14-L188) - [inputremoting.js:63-300](file://client/src/inputremoting.js#L63-L300) - [gamepadhandler.js:1-45](file://client/src/gamepadhandler.js#L1-L45) - [pointercorrect.js:6-125](file://client/src/pointercorrect.js#L6-L125) - [memoryhelper.js:1-28](file://client/src/memoryhelper.js#L1-L28) - [keymap.js:1-120](file://client/src/keymap.js#L1-L120) - [charnumber.js:1-110](file://client/src/charnumber.js#L1-L110) - [mousebutton.js:1-7](file://client/src/mousebutton.js#L1-L7) - [gamepadbutton.js:1-26](file://client/src/gamepadbutton.js#L1-L26) - [touchphase.js:1-9](file://client/src/touchphase.js#L1-L9) - [touchflags.js:1-8](file://client/src/touchflags.js#L1-L8) ## 架构总览 下图展示了从用户输入到远端重放的完整链路:DOM 事件经 Sender 转换为设备状态,再封装为 StateEvent/TextEvent,通过 InputRemoting 序列化为 Message,经 WebRTC 数据通道发送至对端。 ```mermaid sequenceDiagram participant U as "用户" participant DOM as "DOM事件" participant S as "Sender" participant DEV as "设备/状态" participant IR as "InputRemoting" participant MSG as "Message/NewEventsMsg" participant RS as "RenderStreaming" participant PEER as "Peer" participant DC as "RTC数据通道" U->>DOM : 鼠标/键盘/触摸/手柄事件 DOM->>S : 事件回调 S->>DEV : queueEvent(...) / 更新currentState S->>IR : 触发自定义事件(event) IR->>MSG : 创建NewEventsMsg/TextEvent MSG->>RS : 发送消息 RS->>PEER : 选择Peer/数据通道 PEER->>DC : send(Message.buffer) DC-->>PEER : 传输完成 PEER-->>RS : 对端接收 ``` 图表来源 - [sender.js:121-182](file://client/src/sender.js#L121-L182) - [inputremoting.js:73-168](file://client/src/inputremoting.js#L73-L168) - [renderstreaming.js:260-266](file://client/src/renderstreaming.js#L260-L266) - [peer.js:116-122](file://client/src/peer.js#L116-L122) ## 详细组件分析 ### 输入设备抽象层与事件模型 - 设备与状态 - InputDevice:持有设备元信息与当前状态,提供 queueEvent 接口 - IInputState:定义 buffer/format 抽象,各设备状态实现序列化 - 具体设备:Mouse、Keyboard、Touchscreen、Gamepad 分别在 queueEvent 中更新 currentState - 事件模型 - InputEvent:统一头部字段(type、size、deviceId、time),作为所有事件的基类 - StateEvent:在 InputEvent 基础上携带 stateFormat 与 stateData,承载完整输入状态 - TextEvent:在 InputEvent 基础上携带字符编码,用于文本输入场景 - 数据结构与格式 - 四字节标识(FourCC)用于区分不同状态格式(如 MOUSE、KEYS、TOUC、TSRC、GPAD、TEXT、STAT) - 各状态结构严格定义字段偏移与大小,确保跨端一致解析 ```mermaid classDiagram class InputDevice { +name +layout +deviceId +usages +description +updateState(state) +queueEvent(event) +currentState } class IInputState { +buffer +format } class MouseState class KeyboardState class TouchscreenState class GamepadState class InputEvent { +type +sizeInBytes +deviceId +time +eventId +buffer } class StateEvent { +baseEvent : InputEvent +stateFormat +stateData +buffer } class TextEvent { +baseEvent : InputEvent +character +buffer } InputDevice --> IInputState : "持有currentState" IInputState <|-- MouseState IInputState <|-- KeyboardState IInputState <|-- TouchscreenState IInputState <|-- GamepadState InputEvent <|-- StateEvent InputEvent <|-- TextEvent ``` 图表来源 - [inputdevice.js:40-719](file://client/src/inputdevice.js#L40-L719) 章节来源 - [inputdevice.js:40-719](file://client/src/inputdevice.js#L40-L719) ### 输入采集与坐标映射(Sender 与 PointerCorrector) - 事件采集 - Sender 注册鼠标、键盘、触摸、手柄事件监听,调用对应设备的 queueEvent - 鼠标移动位置经 PointerCorrector 映射到视频区域坐标后,再生成 StateEvent - 触摸事件逐个触点复制并映射坐标后分别生成 StateEvent - 坐标映射 - PointerCorrector 计算 letterbox 类型与尺寸,将页面坐标转换为视频画布坐标 - 支持动态宽高变化与元素尺寸观测 ```mermaid flowchart TD Start(["事件到达"]) --> Type{"事件类型?"} Type --> |鼠标| Map["PointerCorrector.map(pos)"] Type --> |触摸| Copy["遍历触点并复制状态"] Map --> Gen["生成StateEvent并广播"] Copy --> Map2["逐点映射坐标"] Map2 --> Gen Type --> |键盘| KGen["生成StateEvent/TextEvent并广播"] Type --> |手柄| GGen["生成StateEvent并广播"] Gen --> End(["完成"]) KGen --> End GGen --> End ``` 图表来源 - [sender.js:121-182](file://client/src/sender.js#L121-L182) - [pointercorrect.js:20-40](file://client/src/pointercorrect.js#L20-L40) 章节来源 - [sender.js:121-182](file://client/src/sender.js#L121-L182) - [pointercorrect.js:6-125](file://client/src/pointercorrect.js#L6-L125) ### 远程转发与消息封装(InputRemoting 与 Message) - 事件订阅与初始消息 - InputRemoting 订阅 LocalInputManager 的事件与设备变更事件 - 启动时发送设备列表与布局初始消息 - 消息类型与封装 - Message 统一头部:participant_id、type、length、data - NewEventsMsg/TextEvent/StateEvent 将事件与状态序列化为二进制 - NewDeviceMsg/RemoveDeviceMsg/ChangeUsageMsg 提供设备生命周期消息 - 发送路径 - InputRemoting 将消息分发给观察者(如 RTC 数据通道) ```mermaid sequenceDiagram participant L as "LocalInputManager" participant IR as "InputRemoting" participant OBS as "观察者(如RTC数据通道)" participant M as "Message/NewEventsMsg" L-->>IR : 触发'event'事件 IR->>M : 创建NewEventsMsg/TextEvent IR->>OBS : onNext(Message) OBS-->>IR : 发送完成 ``` 图表来源 - [inputremoting.js:73-168](file://client/src/inputremoting.js#L73-L168) - [inputremoting.js:184-232](file://client/src/inputremoting.js#L184-L232) - [inputremoting.js:258-277](file://client/src/inputremoting.js#L258-L277) 章节来源 - [inputremoting.js:63-300](file://client/src/inputremoting.js#L63-L300) ### 游戏手柄事件处理(GamepadHandler) - 轮询与事件 - GamepadHandler 使用 requestAnimationFrame 周期扫描已连接手柄 - 对每个控制器派发自定义 gamepadupdated 事件,Sender 作为监听器接收并转发 - 按钮与摇杆 - GamepadState 将按钮 pressed/value 与摇杆 axes 转换为位掩码与浮点值 ```mermaid sequenceDiagram participant GH as "GamepadHandler" participant W as "window" participant S as "Sender" participant DEV as "Gamepad设备" GH->>GH : _scanGamepad() GH->>W : requestAnimationFrame(_updateStatus) GH->>S : dispatchEvent('gamepadupdated') S->>DEV : queueEvent(event) S->>IR : 生成StateEvent并发送 ``` 图表来源 - [gamepadhandler.js:22-42](file://client/src/gamepadhandler.js#L22-L42) - [sender.js:152-168](file://client/src/sender.js#L152-L168) 章节来源 - [gamepadhandler.js:1-45](file://client/src/gamepadhandler.js#L1-L45) - [sender.js:68-85](file://client/src/sender.js#L68-L85) ### 键盘与字符事件映射 - 键位映射 - Keymap 将 KeyboardEvent.code 映射到内部键索引 - MemoryHelper.writeSingleBit 将按键状态写入位数组 - 文本事件 - CharNumber 将 KeyboardEvent.key 映射为字符编码 - TextEvent 在 InputEvent 基础上附加字符编码 章节来源 - [keymap.js:1-120](file://client/src/keymap.js#L1-L120) - [memoryhelper.js:7-20](file://client/src/memoryhelper.js#L7-L20) - [charnumber.js:1-110](file://client/src/charnumber.js#L1-L110) - [inputdevice.js:620-660](file://client/src/inputdevice.js#L620-L660) ### 触摸事件与多指支持 - 触摸阶段与标志 - TouchPhase 定义Began/Moved/Ended/Canceled/Stationary - TouchFlags 定义 IndirectTouch、PrimaryTouch、Tap 等标志 - 多触点状态 - TouchscreenState 将 TouchEvent.changedTouches 转换为 TouchState 列表 - 为每个触点分配唯一 touchId,缓存前一帧状态以计算 delta 章节来源 - [touchphase.js:1-9](file://client/src/touchphase.js#L1-L9) - [touchflags.js:1-8](file://client/src/touchflags.js#L1-L8) - [inputdevice.js:324-538](file://client/src/inputdevice.js#L324-L538) ### WebRTC 信令与数据通道(传输层) - 信令 - Signaling/WebSocketSignaling 负责 offer/answer/candidate/on-message 传递 - RenderStreaming 管理多参与方连接与 Peer 生命周期 - 数据通道 - Peer 封装 RTCPeerConnection,暴露 addDataChannel/createDataChannel - Sender 通过 RenderStreaming.createDataChannel 获取数据通道,发送 Message.buffer ```mermaid sequenceDiagram participant RS as "RenderStreaming" participant PEER as "Peer" participant DC as "RTC数据通道" participant IR as "InputRemoting" participant MSG as "Message" RS->>PEER : createDataChannel(label) PEER-->>RS : 返回数据通道 IR->>MSG : 序列化消息 IR->>DC : send(Message.buffer) DC-->>IR : 发送成功 ``` 图表来源 - [renderstreaming.js:260-266](file://client/src/renderstreaming.js#L260-L266) - [peer.js:116-122](file://client/src/peer.js#L116-L122) - [inputremoting.js:164-168](file://client/src/inputremoting.js#L164-L168) 章节来源 - [renderstreaming.js:11-317](file://client/src/renderstreaming.js#L11-L317) - [peer.js:3-188](file://client/src/peer.js#L3-L188) - [signaling.js:3-292](file://client/src/signaling.js#L3-L292) ## 依赖关系分析 - 组件耦合 - Sender 依赖 InputDevice/IInputState、PointerCorrector、GamepadHandler - InputRemoting 依赖 Message/NewEventsMsg/NewDeviceMsg 以及观察者接口 - RenderStreaming/Peer/Signaling 提供传输基础设施 - 关键依赖链 - DOM 事件 → Sender → 设备状态 → InputRemoting → Message → 数据通道 → 对端 - 潜在循环依赖 - 当前模块间为单向依赖,未见循环导入 ```mermaid graph LR DOM["DOM事件"] --> S["Sender"] S --> ID["InputDevice/IInputState"] S --> PC["PointerCorrector"] S --> GH["GamepadHandler"] ID --> IE["InputEvent/StateEvent/TextEvent"] IE --> IR["InputRemoting"] IR --> MSG["Message/NewEventsMsg"] MSG --> RS["RenderStreaming"] RS --> PEER["Peer"] RS --> SIG["Signaling"] ``` 图表来源 - [sender.js:14-188](file://client/src/sender.js#L14-L188) - [inputremoting.js:63-300](file://client/src/inputremoting.js#L63-L300) - [renderstreaming.js:11-317](file://client/src/renderstreaming.js#L11-L317) 章节来源 - [sender.js:14-188](file://client/src/sender.js#L14-L188) - [inputremoting.js:63-300](file://client/src/inputremoting.js#L63-L300) - [renderstreaming.js:11-317](file://client/src/renderstreaming.js#L11-L317) ## 性能考量 - 事件采样与批处理 - 建议在 Sender 层对高频事件(如 mousemove、touchmove)进行节流/去抖,降低序列化与网络负载 - 序列化开销 - 使用 DataView/ArrayBuffer 直接写入,避免 JSON 序列化带来的额外开销 - 状态压缩 - 键盘状态采用位数组存储,减少带宽占用 - 坐标映射 - PointerCorrector 在每次事件时执行,建议缓存元素尺寸并在 resize 时更新 - 传输可靠性 - 数据通道为有序可靠传输,适合输入事件;若需低延迟可考虑自定义协议或 UDP 通道(需服务端配合) ## 故障排查指南 - 事件未发送 - 检查 InputRemoting 是否已 startSending,是否正确订阅 LocalInputManager 的事件 - 确认观察者(数据通道)处于 open 状态 - 坐标异常 - 确认 PointerCorrector 的 videoWidth/videoHeight 与 HTMLVideoElement 的实际尺寸一致 - 检查 letterbox 计算逻辑与 contentRect 更新 - 键盘状态不同步 - 确保 Keymap 中包含对应 KeyboardEvent.code - 检查 MemoryHelper.writeSingleBit 的位偏移与状态合并逻辑 - 触摸多指错乱 - 确认 TouchState.incrementTouchId 与 prevTouches 缓存逻辑 - 检查 TouchPhase 转换与 tapCount 标记 - 手柄无响应 - 确认 GamepadHandler 正在派发 gamepadupdated 事件 - 检查 GamepadState 按钮位与摇杆轴值映射 章节来源 - [inputremoting.js:73-98](file://client/src/inputremoting.js#L73-L98) - [sender.js:114-182](file://client/src/sender.js#L114-L182) - [pointercorrect.js:71-124](file://client/src/pointercorrect.js#L71-L124) - [memoryhelper.js:7-20](file://client/src/memoryhelper.js#L7-L20) - [keymap.js:1-120](file://client/src/keymap.js#L1-L120) - [touchphase.js:1-9](file://client/src/touchphase.js#L1-L9) - [touchflags.js:1-8](file://client/src/touchflags.js#L1-L8) - [gamepadhandler.js:22-42](file://client/src/gamepadhandler.js#L22-L42) - [inputdevice.js:540-618](file://client/src/inputdevice.js#L540-L618) ## 结论 本系统通过清晰的输入抽象层与严格的二进制事件格式,实现了鼠标、键盘、触摸与手柄的统一采集、转换与远程转发。Sender 负责事件采集与坐标映射,InputRemoting 负责消息封装与分发,RenderStreaming/Peer/Signaling 提供可靠的传输通道。通过时间戳与位掩码等设计,系统具备良好的跨设备同步基础。后续可在事件节流、状态压缩与延迟补偿方面进一步优化,以满足更高实时性需求。 ## 附录 - 时间戳与事件队列 - Sender 使用 timeSinceStartup 作为事件时间戳,保证相对时间一致性 - 建议对端按接收时间与发送时间差进行延迟补偿 - 设备生命周期 - InputRemoting 支持设备新增/移除/使用变更消息,可用于对端设备同步 - 按键与按钮位定义 - MouseButton/GamepadButton 提供统一位索引,便于跨平台一致映射 章节来源 - [sender.js:39-41](file://client/src/sender.js#L39-L41) - [inputremoting.js:108-136](file://client/src/inputremoting.js#L108-L136) - [mousebutton.js:1-7](file://client/src/mousebutton.js#L1-L7) - [gamepadbutton.js:1-26](file://client/src/gamepadbutton.js#L1-L26)