# 输入设备控制

<cite>
**本文引用的文件**
- [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)
</cite>

## 目录
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<br/>事件采集与分发"]
GP["GamepadHandler<br/>手柄轮询"]
PC["PointerCorrector<br/>坐标映射"]
end
subgraph "输入抽象层"
ID["InputDevice/IInputState<br/>设备与状态模型"]
IE["InputEvent/StateEvent/TextEvent<br/>事件与状态封装"]
end
subgraph "远程转发层"
IR["InputRemoting<br/>事件订阅与发送"]
MSG["Message/NewEventsMsg/NewDeviceMsg<br/>消息封装"]
end
subgraph "传输层"
RS["RenderStreaming<br/>信令与Peer管理"]
PEER["Peer<br/>RTCPeerConnection封装"]
SIG["Signaling/WebSocketSignaling<br/>信令通道"]
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)