Files
video_socket-server/.qoder/repowiki/zh/content/WebRTC 实现/输入设备控制/输入设备控制.md

472 lines
18 KiB
Markdown
Raw Normal View History

2026-05-16 13:24:02 +08:00
# 输入设备控制
<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)