修改
This commit is contained in:
385
.qoder/repowiki/zh/content/WebRTC 实现/输入设备控制/触摸输入处理.md
Normal file
385
.qoder/repowiki/zh/content/WebRTC 实现/输入设备控制/触摸输入处理.md
Normal file
@@ -0,0 +1,385 @@
|
||||
# 触摸输入处理
|
||||
|
||||
<cite>
|
||||
**本文引用的文件**
|
||||
- [touchflags.js](file://client/src/touchflags.js)
|
||||
- [touchphase.js](file://client/src/touchphase.js)
|
||||
- [inputdevice.js](file://client/src/inputdevice.js)
|
||||
- [inputremoting.js](file://client/src/inputremoting.js)
|
||||
- [memoryhelper.js](file://client/src/memoryhelper.js)
|
||||
- [pointercorrect.js](file://client/src/pointercorrect.js)
|
||||
- [register-events.js](file://client/public/videoplayer/js/register-events.js)
|
||||
- [video-player.js](file://client/public/videoplayer/js/video-player.js)
|
||||
- [main.js](file://client/public/videoplayer/js/main.js)
|
||||
- [inputdevice.test.js](file://client/test/inputdevice.test.js)
|
||||
- [inputremoting.test.js](file://client/test/inputremoting.test.js)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考量](#性能考量)
|
||||
8. [故障排查指南](#故障排查指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件聚焦于移动端触摸输入的处理机制,涵盖触摸事件的捕获与转发、触摸点标识、多点触控支持、手势识别基础、坐标系统映射、压力感应与触摸相位管理。文档基于仓库中的触摸输入相关源码,结合测试用例与客户端示例,给出可操作的实现路径与最佳实践。
|
||||
|
||||
## 项目结构
|
||||
触摸输入处理涉及三层:
|
||||
- 输入设备建模层:定义触摸状态、事件格式与序列化协议
|
||||
- 输入远程转发层:将本地输入事件打包并通过数据通道发送
|
||||
- 客户端事件绑定层:在页面元素上注册触摸事件监听并进行坐标映射
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "输入设备建模层"
|
||||
A["touchphase.js<br/>触摸相位常量"]
|
||||
B["touchflags.js<br/>触摸标志位"]
|
||||
C["inputdevice.js<br/>Touchscreen/ToucnState/TouchnscreenState<br/>MemoryHelper"]
|
||||
end
|
||||
subgraph "输入远程转发层"
|
||||
D["inputremoting.js<br/>InputRemoting/NewEventsMsg/Message"]
|
||||
end
|
||||
subgraph "客户端事件绑定层"
|
||||
E["register-events.js<br/>注册触摸事件/坐标映射"]
|
||||
F["video-player.js<br/>计算视频区域偏移与缩放"]
|
||||
G["main.js<br/>设置元素 touch-action:none"]
|
||||
end
|
||||
A --> C
|
||||
B --> C
|
||||
C --> D
|
||||
D --> E
|
||||
F --> E
|
||||
G --> E
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [touchphase.js:1-9](file://client/src/touchphase.js#L1-L9)
|
||||
- [touchflags.js:1-8](file://client/src/touchflags.js#L1-L8)
|
||||
- [inputdevice.js:111-118](file://client/src/inputdevice.js#L111-L118)
|
||||
- [inputremoting.js:63-91](file://client/src/inputremoting.js#L63-L91)
|
||||
- [register-events.js:161-281](file://client/public/videoplayer/js/register-events.js#L161-L281)
|
||||
- [video-player.js:157-213](file://client/public/videoplayer/js/video-player.js#L157-L213)
|
||||
- [main.js:58-65](file://client/public/videoplayer/js/main.js#L58-L65)
|
||||
|
||||
**章节来源**
|
||||
- [inputdevice.js:111-118](file://client/src/inputdevice.js#L111-L118)
|
||||
- [inputremoting.js:63-91](file://client/src/inputremoting.js#L63-L91)
|
||||
- [register-events.js:161-281](file://client/public/videoplayer/js/register-events.js#L161-L281)
|
||||
- [video-player.js:157-213](file://client/public/videoplayer/js/video-player.js#L157-L213)
|
||||
- [main.js:58-65](file://client/public/videoplayer/js/main.js#L58-L65)
|
||||
|
||||
## 核心组件
|
||||
- 触摸相位与标志
|
||||
- 相位:None、Began、Moved、Ended、Canceled、Stationary
|
||||
- 标志:IndirectTouch、PrimaryTouch、Tap、OrphanedPrimaryTouch
|
||||
- 触摸状态建模
|
||||
- TouchState:包含触摸点唯一ID、位置、增量、压力、半径、相位、点击次数、显示索引、标志、起始时间与起始位置等字段
|
||||
- TouchscreenState:聚合多个TouchState,负责将TouchEvent转换为内部状态数组
|
||||
- 输入事件序列化
|
||||
- TouchState.buffer与TouchscreenState.buffer将状态编码为二进制帧
|
||||
- 输入远程转发
|
||||
- InputRemoting监听本地输入事件,通过NewEventsMsg封装后经订阅者发送
|
||||
- 客户端事件绑定与坐标映射
|
||||
- 在播放器元素上注册touchstart/move/end/cancel事件
|
||||
- 使用PointerCorrector或VideoPlayer提供的缩放与偏移参数进行坐标映射
|
||||
|
||||
**章节来源**
|
||||
- [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-453](file://client/src/inputdevice.js#L324-L453)
|
||||
- [inputdevice.js:455-538](file://client/src/inputdevice.js#L455-L538)
|
||||
- [inputremoting.js:138-141](file://client/src/inputremoting.js#L138-L141)
|
||||
- [register-events.js:161-281](file://client/public/videoplayer/js/register-events.js#L161-L281)
|
||||
- [video-player.js:157-213](file://client/public/videoplayer/js/video-player.js#L157-L213)
|
||||
|
||||
## 架构总览
|
||||
触摸事件从浏览器到远端的典型流程如下:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as "用户"
|
||||
participant V as "VideoPlayer<br/>video-player.js"
|
||||
participant R as "注册函数<br/>register-events.js"
|
||||
participant S as "序列化<br/>inputdevice.js"
|
||||
participant M as "远程转发<br/>inputremoting.js"
|
||||
U->>R : "触摸事件 : touchstart/move/end/cancel"
|
||||
R->>R : "收集changedTouches与touches"
|
||||
R->>V : "读取videoScale/videoOriginX/Y"
|
||||
R->>R : "坐标映射与相位判定"
|
||||
R->>S : "构造TouchState/TouchnscreenState"
|
||||
S-->>R : "buffer(二进制)"
|
||||
R->>M : "NewEventsMsg.create(state)"
|
||||
M-->>M : "Message封装"
|
||||
M-->>远端 : "发送二进制帧"
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [register-events.js:161-281](file://client/public/videoplayer/js/register-events.js#L161-L281)
|
||||
- [video-player.js:157-213](file://client/public/videoplayer/js/video-player.js#L157-L213)
|
||||
- [inputdevice.js:455-538](file://client/src/inputdevice.js#L455-L538)
|
||||
- [inputremoting.js:138-141](file://client/src/inputremoting.js#L138-L141)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 触摸相位与标志
|
||||
- 相位常量用于统一表示触摸生命周期各阶段,便于跨平台一致性处理
|
||||
- 标志位用于表达触摸属性(如主触摸、点击标记等),便于后续手势合成与策略判断
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class TouchPhase {
|
||||
+None
|
||||
+Began
|
||||
+Moved
|
||||
+Ended
|
||||
+Canceled
|
||||
+Stationary
|
||||
}
|
||||
class TouchFlags {
|
||||
+IndirectTouch
|
||||
+PrimaryTouch
|
||||
+Tap
|
||||
+OrphanedPrimaryTouch
|
||||
}
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [touchphase.js:1-9](file://client/src/touchphase.js#L1-L9)
|
||||
- [touchflags.js:1-8](file://client/src/touchflags.js#L1-L8)
|
||||
|
||||
**章节来源**
|
||||
- [touchphase.js:1-9](file://client/src/touchphase.js#L1-L9)
|
||||
- [touchflags.js:1-8](file://client/src/touchflags.js#L1-L8)
|
||||
|
||||
### 触摸状态建模与序列化
|
||||
- TouchState字段覆盖位置、增量、压力、半径、相位、起始时间与起始位置等,满足轨迹预测与手势识别需求
|
||||
- TouchscreenState聚合多点触摸,将Web TouchEvent转换为内部状态数组
|
||||
- MemoryHelper提供按位写入能力,支撑布尔型标志位的紧凑存储
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class TouchState {
|
||||
+touchId : number
|
||||
+position : number[]
|
||||
+delta : number[]
|
||||
+pressure : number
|
||||
+radius : number[]
|
||||
+phaseId : number
|
||||
+tapCount : number
|
||||
+displayIndex : number
|
||||
+flags : number
|
||||
+padding : number
|
||||
+startTime : number
|
||||
+startPosition : number[]
|
||||
+buffer() ArrayBuffer
|
||||
+format() number
|
||||
}
|
||||
class TouchscreenState {
|
||||
+touchData : TouchState[]
|
||||
+buffer() ArrayBuffer
|
||||
+format() number
|
||||
+convertPhaseId(type) number
|
||||
}
|
||||
class MemoryHelper {
|
||||
+writeSingleBit(buffer, bitOffset, value) void
|
||||
+sizeOfInt : number
|
||||
}
|
||||
TouchscreenState --> TouchState : "聚合"
|
||||
TouchState --> MemoryHelper : "使用"
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [inputdevice.js:324-453](file://client/src/inputdevice.js#L324-L453)
|
||||
- [inputdevice.js:455-538](file://client/src/inputdevice.js#L455-L538)
|
||||
- [memoryhelper.js:1-28](file://client/src/memoryhelper.js#L1-L28)
|
||||
|
||||
**章节来源**
|
||||
- [inputdevice.js:324-453](file://client/src/inputdevice.js#L324-L453)
|
||||
- [inputdevice.js:455-538](file://client/src/inputdevice.js#L455-L538)
|
||||
- [memoryhelper.js:1-28](file://client/src/memoryhelper.js#L1-L28)
|
||||
|
||||
### 客户端触摸事件绑定与坐标映射
|
||||
- 在播放器元素上注册触摸事件监听,区分Began/Moved/Ended/Canceled
|
||||
- 通过VideoPlayer提供的videoScale、videoOriginX/Y与videoHeight进行坐标映射,适配letterbox与CSS布局
|
||||
- 对于Unity坐标系,需要对Y轴做翻转处理
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["触摸事件触发"]) --> Collect["收集changedTouches与touches"]
|
||||
Collect --> Phase["根据是否存在于touches判定Stationary或对应相位"]
|
||||
Phase --> Map["读取videoScale/videoOriginX/Y与videoHeight"]
|
||||
Map --> Scale["坐标映射: (x-origin)/scale 或 (height-(y-origin))/scale"]
|
||||
Scale --> Pack["构造二进制帧: TouchState/TouchnscreenState"]
|
||||
Pack --> End(["发送至InputRemoting"])
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [register-events.js:161-281](file://client/public/videoplayer/js/register-events.js#L161-L281)
|
||||
- [video-player.js:157-213](file://client/public/videoplayer/js/video-player.js#L157-L213)
|
||||
|
||||
**章节来源**
|
||||
- [register-events.js:161-281](file://client/public/videoplayer/js/register-events.js#L161-L281)
|
||||
- [video-player.js:157-213](file://client/public/videoplayer/js/video-player.js#L157-L213)
|
||||
- [main.js:58-65](file://client/public/videoplayer/js/main.js#L58-L65)
|
||||
|
||||
### 输入远程转发与消息封装
|
||||
- InputRemoting监听本地输入事件,调用NewEventsMsg.create将状态事件封装为Message
|
||||
- Message包含参与者ID、消息类型与数据载荷,供订阅者发送
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant L as "LocalInputManager"
|
||||
participant IR as "InputRemoting"
|
||||
participant NE as "NewEventsMsg"
|
||||
participant MSG as "Message"
|
||||
L->>IR : "onEvent : event"
|
||||
IR->>NE : "create(state)"
|
||||
NE-->>IR : "Message"
|
||||
IR-->>订阅者 : "onNext(Message)"
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [inputremoting.js:79-91](file://client/src/inputremoting.js#L79-L91)
|
||||
- [inputremoting.js:138-141](file://client/src/inputremoting.js#L138-L141)
|
||||
- [inputremoting.js:258-277](file://client/src/inputremoting.js#L258-L277)
|
||||
- [inputremoting.js:184-232](file://client/src/inputremoting.js#L184-L232)
|
||||
|
||||
**章节来源**
|
||||
- [inputremoting.js:63-91](file://client/src/inputremoting.js#L63-L91)
|
||||
- [inputremoting.js:138-141](file://client/src/inputremoting.js#L138-L141)
|
||||
- [inputremoting.js:258-277](file://client/src/inputremoting.js#L258-L277)
|
||||
- [inputremoting.js:184-232](file://client/src/inputremoting.js#L184-L232)
|
||||
|
||||
### 坐标系统与Letterbox处理
|
||||
- PointerCorrector提供从元素坐标到视频坐标的映射,考虑letterbox偏移与缩放
|
||||
- VideoPlayer在resizeVideo时计算videoScale与videoOriginX/Y,确保触摸坐标与渲染区域一致
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class PointerCorrector {
|
||||
+map(position) number[]
|
||||
+reset(w,h,elem) void
|
||||
+contentRect() Object
|
||||
+letterBoxType() number
|
||||
+letterBoxSize() number
|
||||
}
|
||||
class VideoPlayer {
|
||||
+resizeVideo() void
|
||||
+videoScale : number
|
||||
+videoOriginX : number
|
||||
+videoOriginY : number
|
||||
}
|
||||
PointerCorrector --> VideoPlayer : "配合使用"
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [pointercorrect.js:6-124](file://client/src/pointercorrect.js#L6-L124)
|
||||
- [video-player.js:157-213](file://client/public/videoplayer/js/video-player.js#L157-L213)
|
||||
|
||||
**章节来源**
|
||||
- [pointercorrect.js:6-124](file://client/src/pointercorrect.js#L6-L124)
|
||||
- [video-player.js:157-213](file://client/public/videoplayer/js/video-player.js#L157-L213)
|
||||
|
||||
### 多点触控与手势识别基础
|
||||
- TouchscreenState支持最多10个触摸点,通过TouchState.prevTouches缓存上一帧状态,便于计算delta与轨迹
|
||||
- 测试用例验证TouchscreenState能正确生成buffer,表明多点数据可被序列化传输
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
TStart(["touchstart"]) --> Cache["缓存TouchState(prevTouches)"]
|
||||
TM["touchmove"] --> Delta["计算delta与轨迹"]
|
||||
TEnd(["touchend"]) --> Tap["标记点击/结束状态"]
|
||||
Cache --> Delta
|
||||
Delta --> TEnd
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [inputdevice.js:494-516](file://client/src/inputdevice.js#L494-L516)
|
||||
- [inputdevice.test.js:69-100](file://client/test/inputdevice.test.js#L69-L100)
|
||||
|
||||
**章节来源**
|
||||
- [inputdevice.js:455-538](file://client/src/inputdevice.js#L455-L538)
|
||||
- [inputdevice.test.js:69-100](file://client/test/inputdevice.test.js#L69-L100)
|
||||
|
||||
### 压力感应与触摸相位管理
|
||||
- TouchState记录pressure与radius,支持压力与接触面信息
|
||||
- 相位管理由TouchscreenState.convertPhaseId统一转换,保证与TouchPhase一致
|
||||
|
||||
**章节来源**
|
||||
- [inputdevice.js:324-453](file://client/src/inputdevice.js#L324-L453)
|
||||
- [inputdevice.js:455-538](file://client/src/inputdevice.js#L455-L538)
|
||||
|
||||
### 具体代码示例路径
|
||||
- 如何处理移动端触摸事件:[register-events.js:161-281](file://client/public/videoplayer/js/register-events.js#L161-L281)
|
||||
- 触摸轨迹预测与多点触控:[inputdevice.js:455-538](file://client/src/inputdevice.js#L455-L538)
|
||||
- 坐标映射与letterbox处理:[pointercorrect.js:20-40](file://client/src/pointercorrect.js#L20-L40),[video-player.js:157-213](file://client/public/videoplayer/js/video-player.js#L157-L213)
|
||||
- 触摸相位与标志:[touchphase.js:1-9](file://client/src/touchphase.js#L1-L9),[touchflags.js:1-8](file://client/src/touchflags.js#L1-L8)
|
||||
|
||||
## 依赖关系分析
|
||||
- TouchscreenState依赖TouchState与TouchPhase、TouchFlags
|
||||
- TouchState依赖MemoryHelper进行位级写入
|
||||
- InputRemoting依赖NewEventsMsg与Message进行事件封装与发送
|
||||
- 客户端事件绑定依赖VideoPlayer提供的缩放与偏移参数
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
TP["touchphase.js"] --> IDS["inputdevice.js"]
|
||||
TF["touchflags.js"] --> IDS
|
||||
MH["memoryhelper.js"] --> IDS
|
||||
IDS --> IR["inputremoting.js"]
|
||||
VP["video-player.js"] --> RE["register-events.js"]
|
||||
IR --> RE
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [inputdevice.js:9-10](file://client/src/inputdevice.js#L9-L10)
|
||||
- [inputremoting.js:1-8](file://client/src/inputremoting.js#L1-L8)
|
||||
- [register-events.js:1-4](file://client/public/videoplayer/js/register-events.js#L1-L4)
|
||||
|
||||
**章节来源**
|
||||
- [inputdevice.js:9-10](file://client/src/inputdevice.js#L9-L10)
|
||||
- [inputremoting.js:1-8](file://client/src/inputremoting.js#L1-L8)
|
||||
- [register-events.js:1-4](file://client/public/videoplayer/js/register-events.js#L1-L4)
|
||||
|
||||
## 性能考量
|
||||
- 二进制序列化:TouchState/TouchnscreenState采用定长字段与紧凑布局,减少序列化开销
|
||||
- 缓存策略:TouchState.prevTouches缓存上一帧状态,避免重复计算与查找
|
||||
- 事件节流:在高频touchmove场景中,建议按需采样或合并更新,降低发送频率
|
||||
- 坐标映射:在resize频繁的场景,应避免在每帧都重建映射对象,复用VideoPlayer计算结果
|
||||
|
||||
## 故障排查指南
|
||||
- 触摸事件未生效
|
||||
- 检查播放器元素是否设置了touch-action:none,防止默认滚动行为干扰
|
||||
- 确认已注册touchstart/move/end/cancel事件监听
|
||||
- 坐标错位
|
||||
- 确保在resize后调用VideoPlayer.resizeVideo重新计算videoScale与videoOriginX/Y
|
||||
- 检查letterbox类型与偏移是否正确
|
||||
- 压力或半径无效
|
||||
- 确认设备与浏览器支持force/radius属性;若不支持,pressure/radius可能为0
|
||||
- 手势识别异常
|
||||
- 检查TouchState.prevTouches缓存是否正确更新
|
||||
- 确认相位转换逻辑与TouchPhase一致
|
||||
|
||||
**章节来源**
|
||||
- [main.js:58-65](file://client/public/videoplayer/js/main.js#L58-L65)
|
||||
- [video-player.js:157-213](file://client/public/videoplayer/js/video-player.js#L157-L213)
|
||||
- [inputdevice.js:494-516](file://client/src/inputdevice.js#L494-L516)
|
||||
- [touchphase.js:1-9](file://client/src/touchphase.js#L1-L9)
|
||||
|
||||
## 结论
|
||||
该触摸输入处理方案通过清晰的状态建模、稳定的相位与标志体系、高效的二进制序列化以及可靠的客户端坐标映射,实现了移动端触摸事件的可靠捕获与转发。结合多点触控与压力信息,可为进一步的手势识别与轨迹预测奠定基础。建议在高刷场景中引入采样与合并策略,以平衡交互流畅性与网络开销。
|
||||
|
||||
## 附录
|
||||
- 测试用例验证了TouchscreenState的buffer生成与格式正确性,可作为集成测试参考
|
||||
- 客户端示例展示了如何在播放器元素上注册触摸事件并进行坐标映射
|
||||
|
||||
**章节来源**
|
||||
- [inputdevice.test.js:69-100](file://client/test/inputdevice.test.js#L69-L100)
|
||||
- [inputremoting.test.js:59-106](file://client/test/inputremoting.test.js#L59-L106)
|
||||
472
.qoder/repowiki/zh/content/WebRTC 实现/输入设备控制/输入设备控制.md
Normal file
472
.qoder/repowiki/zh/content/WebRTC 实现/输入设备控制/输入设备控制.md
Normal file
@@ -0,0 +1,472 @@
|
||||
# 输入设备控制
|
||||
|
||||
<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)
|
||||
273
.qoder/repowiki/zh/content/WebRTC 实现/输入设备控制/键盘输入处理.md
Normal file
273
.qoder/repowiki/zh/content/WebRTC 实现/输入设备控制/键盘输入处理.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# 键盘输入处理
|
||||
|
||||
<cite>
|
||||
**本文档引用的文件**
|
||||
- [client/src/keymap.js](file://client/src/keymap.js)
|
||||
- [client/src/inputdevice.js](file://client/src/inputdevice.js)
|
||||
- [client/src/inputremoting.js](file://client/src/inputremoting.js)
|
||||
- [client/src/sender.js](file://client/src/sender.js)
|
||||
- [client/src/memoryhelper.js](file://client/src/memoryhelper.js)
|
||||
- [client/src/charnumber.js](file://client/src/charnumber.js)
|
||||
- [client/test/inputdevice.test.js](file://client/test/inputdevice.test.js)
|
||||
- [client/test/inputremoting.test.js](file://client/test/inputremoting.test.js)
|
||||
- [client/public/onebyone/main.js](file://client/public/onebyone/main.js)
|
||||
</cite>
|
||||
|
||||
## 目录
|
||||
1. [简介](#简介)
|
||||
2. [项目结构](#项目结构)
|
||||
3. [核心组件](#核心组件)
|
||||
4. [架构总览](#架构总览)
|
||||
5. [详细组件分析](#详细组件分析)
|
||||
6. [依赖关系分析](#依赖关系分析)
|
||||
7. [性能考虑](#性能考虑)
|
||||
8. [故障排除指南](#故障排除指南)
|
||||
9. [结论](#结论)
|
||||
10. [附录](#附录)
|
||||
|
||||
## 简介
|
||||
本文件系统性地文档化了视频流客户端中的键盘输入处理机制,覆盖按键按下、释放与重复事件的捕获与转发;键盘映射表与按键码转换;特殊键处理;键盘事件状态同步、按键组合检测与输入法支持;以及跨平台键盘布局差异、修饰键状态与跨浏览器兼容性的实践建议。通过代码级分析与可视化图示,帮助开发者在不直接阅读源码的情况下也能快速理解与扩展键盘输入子系统。
|
||||
|
||||
## 项目结构
|
||||
键盘输入处理位于前端客户端模块中,核心文件包括:
|
||||
- 键盘映射与状态:keymap.js、inputdevice.js
|
||||
- 输入设备抽象与事件打包:inputdevice.js、inputremoting.js
|
||||
- 本地事件采集与发送:sender.js
|
||||
- 辅助工具:memoryhelper.js、charnumber.js
|
||||
- 测试用例:inputdevice.test.js、inputremoting.test.js
|
||||
- 应用层快捷键示例:public/onebyone/main.js
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "键盘输入处理模块"
|
||||
KM["Keymap 映射表<br/>keymap.js"]
|
||||
ID["输入设备与状态<br/>inputdevice.js"]
|
||||
IR["输入远程转发<br/>inputremoting.js"]
|
||||
SND["本地采集与发送<br/>sender.js"]
|
||||
MH["内存位操作<br/>memoryhelper.js"]
|
||||
CN["字符编码映射<br/>charnumber.js"]
|
||||
end
|
||||
subgraph "应用层"
|
||||
APP["应用快捷键示例<br/>public/onebyone/main.js"]
|
||||
end
|
||||
KM --> ID
|
||||
MH --> ID
|
||||
CN --> ID
|
||||
ID --> IR
|
||||
SND --> IR
|
||||
APP --> SND
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [client/src/keymap.js:1-120](file://client/src/keymap.js#L1-L120)
|
||||
- [client/src/inputdevice.js:1-719](file://client/src/inputdevice.js#L1-L719)
|
||||
- [client/src/inputremoting.js:1-300](file://client/src/inputremoting.js#L1-L300)
|
||||
- [client/src/sender.js:1-209](file://client/src/sender.js#L1-L209)
|
||||
- [client/src/memoryhelper.js:1-28](file://client/src/memoryhelper.js#L1-L28)
|
||||
- [client/src/charnumber.js:1-110](file://client/src/charnumber.js#L1-L110)
|
||||
- [client/public/onebyone/main.js:132-145](file://client/public/onebyone/main.js#L132-L145)
|
||||
|
||||
**章节来源**
|
||||
- [client/src/keymap.js:1-120](file://client/src/keymap.js#L1-L120)
|
||||
- [client/src/inputdevice.js:1-719](file://client/src/inputdevice.js#L1-L719)
|
||||
- [client/src/inputremoting.js:1-300](file://client/src/inputremoting.js#L1-L300)
|
||||
- [client/src/sender.js:1-209](file://client/src/sender.js#L1-L209)
|
||||
- [client/src/memoryhelper.js:1-28](file://client/src/memoryhelper.js#L1-L28)
|
||||
- [client/src/charnumber.js:1-110](file://client/src/charnumber.js#L1-L110)
|
||||
- [client/public/onebyone/main.js:132-145](file://client/public/onebyone/main.js#L132-L145)
|
||||
|
||||
## 核心组件
|
||||
- 键盘映射表 Keymap:将 DOM KeyboardEvent.code 转换为内部键位索引,统一不同键盘布局下的按键标识。
|
||||
- 键盘状态 KeyboardState:基于位图记录每个键的按下/释放状态,支持按键重复事件的忽略与文本事件的生成。
|
||||
- 输入设备 Keyboard:封装键盘设备描述与事件队列,负责将 DOM 事件转换为内部状态。
|
||||
- 输入远程转发 InputRemoting:订阅本地事件,打包为消息并通过观察者发送。
|
||||
- 本地采集 Sender:注册文档级键盘事件监听,区分重复事件,生成状态事件与文本事件,并通过自定义事件广播。
|
||||
- 内存辅助 MemoryHelper:提供按位写入能力,用于高效更新键位状态位图。
|
||||
- 字符编码 CharNumber:将 DOM KeyboardEvent.key 映射为字符编码,用于文本事件。
|
||||
|
||||
**章节来源**
|
||||
- [client/src/keymap.js:1-120](file://client/src/keymap.js#L1-L120)
|
||||
- [client/src/inputdevice.js:100-322](file://client/src/inputdevice.js#L100-L322)
|
||||
- [client/src/inputremoting.js:63-169](file://client/src/inputremoting.js#L63-L169)
|
||||
- [client/src/sender.js:14-188](file://client/src/sender.js#L14-L188)
|
||||
- [client/src/memoryhelper.js:1-28](file://client/src/memoryhelper.js#L1-L28)
|
||||
- [client/src/charnumber.js:1-110](file://client/src/charnumber.js#L1-L110)
|
||||
|
||||
## 架构总览
|
||||
键盘输入从 DOM 事件开始,经由 Sender 采集,生成 KeyboardState 与可选的 TextEvent,再由 InputRemoting 打包并通过 RTC 数据通道发送至远端。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant DOM as "DOM 文档"
|
||||
participant SND as "Sender"
|
||||
participant KB as "Keyboard 设备"
|
||||
participant KS as "KeyboardState"
|
||||
participant IR as "InputRemoting"
|
||||
participant OBS as "Observer"
|
||||
DOM->>SND : "keydown/keyup 事件"
|
||||
SND->>SND : "区分重复事件与文本事件"
|
||||
SND->>KB : "queueEvent(KeyboardEvent)"
|
||||
KB->>KS : "构造状态(按键按下/释放)"
|
||||
SND->>IR : "派发 StateEvent/TextEvent 自定义事件"
|
||||
IR->>IR : "NewEventsMsg.create(...) 打包"
|
||||
IR-->>OBS : "onNext(Message)"
|
||||
OBS-->>DOM : "RTC 数据通道发送"
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [client/src/sender.js:130-143](file://client/src/sender.js#L130-L143)
|
||||
- [client/src/inputdevice.js:100-109](file://client/src/inputdevice.js#L100-L109)
|
||||
- [client/src/inputdevice.js:274-322](file://client/src/inputdevice.js#L274-L322)
|
||||
- [client/src/inputremoting.js:138-141](file://client/src/inputremoting.js#L138-L141)
|
||||
- [client/src/inputremoting.js:258-277](file://client/src/inputremoting.js#L258-L277)
|
||||
- [client/src/sender.js:170-182](file://client/src/sender.js#L170-L182)
|
||||
|
||||
## 详细组件分析
|
||||
|
||||
### 键盘映射表与按键码转换
|
||||
- Keymap 将标准的 KeyboardEvent.code 映射到连续整数索引,确保不同键盘布局下按键的统一识别。
|
||||
- MemoryHelper.writeSingleBit 使用位图方式更新状态,避免逐键遍历,提升性能。
|
||||
- CharNumber 提供 KeyboardEvent.key 到字符编码的映射,用于 TextEvent 的字符字段。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["按键事件进入"]) --> GetCode["读取 KeyboardEvent.code"]
|
||||
GetCode --> LookupKM["Keymap 查找键位索引"]
|
||||
LookupKM --> BitOp["MemoryHelper.writeSingleBit 更新位图"]
|
||||
BitOp --> Done(["完成状态更新"])
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [client/src/keymap.js:1-120](file://client/src/keymap.js#L1-L120)
|
||||
- [client/src/memoryhelper.js:7-20](file://client/src/memoryhelper.js#L7-L20)
|
||||
- [client/src/inputdevice.js:287-307](file://client/src/inputdevice.js#L287-L307)
|
||||
|
||||
**章节来源**
|
||||
- [client/src/keymap.js:1-120](file://client/src/keymap.js#L1-L120)
|
||||
- [client/src/memoryhelper.js:1-28](file://client/src/memoryhelper.js#L1-L28)
|
||||
- [client/src/charnumber.js:1-110](file://client/src/charnumber.js#L1-L110)
|
||||
- [client/src/inputdevice.js:274-322](file://client/src/inputdevice.js#L274-L322)
|
||||
|
||||
### 键盘事件状态同步与重复事件处理
|
||||
- Sender 在 _onKeyEvent 中区分事件类型与重复标志,仅在非重复的 keydown 时生成 StateEvent,同时始终生成 TextEvent,保证文本输入与状态同步。
|
||||
- KeyboardState 根据事件类型设置对应键位的布尔值,并通过位图持久化当前状态,支持后续状态查询与同步。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
EvtIn(["keydown/keyup 进入"]) --> Type{"事件类型"}
|
||||
Type --> |keydown| Repeat{"是否重复?"}
|
||||
Repeat --> |是| SkipState["跳过 StateEvent"]
|
||||
Repeat --> |否| GenState["生成 StateEvent"]
|
||||
Type --> |keyup| UpState["生成 StateEvent"]
|
||||
GenState --> GenText["生成 TextEvent"]
|
||||
UpState --> GenText
|
||||
SkipState --> End(["结束"])
|
||||
GenText --> End
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [client/src/sender.js:130-143](file://client/src/sender.js#L130-L143)
|
||||
- [client/src/inputdevice.js:287-307](file://client/src/inputdevice.js#L287-L307)
|
||||
|
||||
**章节来源**
|
||||
- [client/src/sender.js:130-143](file://client/src/sender.js#L130-L143)
|
||||
- [client/src/inputdevice.js:274-322](file://client/src/inputdevice.js#L274-L322)
|
||||
|
||||
### 特殊键与修饰键处理
|
||||
- 特殊键(如功能键、方向键、锁定键)通过 Keymap 统一映射,确保跨平台一致性。
|
||||
- 修饰键(Shift/Alt/Control/Meta)同样映射到固定索引,便于在位图中跟踪其状态。
|
||||
- 应用层示例展示了如何在特定上下文中拦截修饰键组合(例如 Ctrl+V),体现修饰键在用户交互中的作用。
|
||||
|
||||
**章节来源**
|
||||
- [client/src/keymap.js:52-66](file://client/src/keymap.js#L52-L66)
|
||||
- [client/public/onebyone/main.js:140-144](file://client/public/onebyone/main.js#L140-L144)
|
||||
|
||||
### 文本事件与输入法支持
|
||||
- TextEvent 通过 CharNumber 将 KeyboardEvent.key 转换为字符编码,作为文本输入的载体。
|
||||
- 该机制独立于键盘状态位图,确保即使在某些平台或浏览器上按键重复导致状态事件被跳过,文本仍能正确传递。
|
||||
|
||||
**章节来源**
|
||||
- [client/src/inputdevice.js:620-660](file://client/src/inputdevice.js#L620-L660)
|
||||
- [client/src/charnumber.js:1-110](file://client/src/charnumber.js#L1-L110)
|
||||
|
||||
### 跨平台键盘布局差异与兼容性
|
||||
- 使用 KeyboardEvent.code 作为按键标识,避免因不同键盘布局导致的 key 值变化,提高跨平台一致性。
|
||||
- 对于需要根据 key 值进行语义判断的场景(如应用快捷键),可结合 KeyboardEvent.key 与修饰键状态进行综合判断。
|
||||
- 针对输入法候选窗口等复杂输入流程,建议在应用层通过监听 compositionstart/compositionupdate/compositionend 等事件进行补充处理(当前键盘子系统主要关注按键与文本事件)。
|
||||
|
||||
**章节来源**
|
||||
- [client/src/keymap.js:1-120](file://client/src/keymap.js#L1-L120)
|
||||
- [client/public/onebyone/main.js:132-145](file://client/public/onebyone/main.js#L132-L145)
|
||||
|
||||
## 依赖关系分析
|
||||
键盘输入处理模块内部依赖清晰,职责分离明确:
|
||||
- keymap.js 与 charnumber.js 为数据层,提供映射与编码。
|
||||
- inputdevice.js 为核心业务层,封装设备与状态。
|
||||
- memoryhelper.js 提供底层位操作能力。
|
||||
- sender.js 负责事件采集与分发。
|
||||
- inputremoting.js 负责消息打包与传输。
|
||||
- 测试用例验证关键行为与格式。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
KM["keymap.js"] --> ID["inputdevice.js"]
|
||||
CN["charnumber.js"] --> ID
|
||||
MH["memoryhelper.js"] --> ID
|
||||
ID --> IR["inputremoting.js"]
|
||||
SND["sender.js"] --> IR
|
||||
TEST1["inputdevice.test.js"] --> ID
|
||||
TEST2["inputremoting.test.js"] --> IR
|
||||
```
|
||||
|
||||
**图表来源**
|
||||
- [client/src/keymap.js:1-120](file://client/src/keymap.js#L1-L120)
|
||||
- [client/src/charnumber.js:1-110](file://client/src/charnumber.js#L1-L110)
|
||||
- [client/src/memoryhelper.js:1-28](file://client/src/memoryhelper.js#L1-L28)
|
||||
- [client/src/inputdevice.js:1-719](file://client/src/inputdevice.js#L1-L719)
|
||||
- [client/src/inputremoting.js:1-300](file://client/src/inputremoting.js#L1-L300)
|
||||
- [client/src/sender.js:1-209](file://client/src/sender.js#L1-L209)
|
||||
- [client/test/inputdevice.test.js:1-173](file://client/test/inputdevice.test.js#L1-L173)
|
||||
- [client/test/inputremoting.test.js:1-132](file://client/test/inputremoting.test.js#L1-L132)
|
||||
|
||||
**章节来源**
|
||||
- [client/src/inputdevice.js:1-719](file://client/src/inputdevice.js#L1-L719)
|
||||
- [client/src/inputremoting.js:1-300](file://client/src/inputremoting.js#L1-L300)
|
||||
- [client/src/sender.js:1-209](file://client/src/sender.js#L1-L209)
|
||||
- [client/test/inputdevice.test.js:1-173](file://client/test/inputdevice.test.js#L1-L173)
|
||||
- [client/test/inputremoting.test.js:1-132](file://client/test/inputremoting.test.js#L1-L132)
|
||||
|
||||
## 性能考虑
|
||||
- 位图存储:KeyboardState 使用位图存储键位状态,空间紧凑且更新高效,适合高频事件场景。
|
||||
- 重复事件过滤:Sender 在 keydown 上跳过重复事件,减少冗余状态事件,降低带宽占用。
|
||||
- 批量发送:InputRemoting 支持批量订阅者推送,便于扩展到多路远端接收。
|
||||
- 建议:对于高密度输入场景,可进一步引入节流/去抖策略或增量状态压缩,以平衡实时性与网络开销。
|
||||
|
||||
[本节为通用性能讨论,无需具体文件引用]
|
||||
|
||||
## 故障排除指南
|
||||
- 事件未触发:检查 Sender 是否正确注册文档级键盘事件监听,确认事件冒泡路径与目标元素。
|
||||
- 文本缺失:若仅发送状态事件而无文本事件,需确认是否正确生成 TextEvent(当前实现会在 keydown 时总是生成文本事件)。
|
||||
- 键位异常:核对 Keymap 中是否存在目标 code 的映射,确保跨平台一致性。
|
||||
- 修饰键误判:应用层快捷键应显式检查修饰键状态,避免在输入法候选等场景误触发。
|
||||
- 测试验证:可通过单元测试验证状态事件与文本事件的格式与内容,确保打包与解析一致。
|
||||
|
||||
**章节来源**
|
||||
- [client/src/sender.js:130-143](file://client/src/sender.js#L130-L143)
|
||||
- [client/src/inputdevice.js:620-660](file://client/src/inputdevice.js#L620-L660)
|
||||
- [client/test/inputdevice.test.js:135-144](file://client/test/inputdevice.test.js#L135-L144)
|
||||
- [client/test/inputremoting.test.js:50-57](file://client/test/inputremoting.test.js#L50-L57)
|
||||
|
||||
## 结论
|
||||
该键盘输入处理子系统通过标准化的按键映射、高效的位图状态管理与清晰的事件分发链路,实现了跨平台、低延迟的键盘输入远程转发。配合应用层的修饰键与快捷键处理,能够满足视频通话等实时交互场景的需求。建议在后续迭代中增强输入法候选窗口支持与状态压缩优化,以进一步提升兼容性与性能。
|
||||
|
||||
[本节为总结性内容,无需具体文件引用]
|
||||
|
||||
## 附录
|
||||
- 代码片段路径参考(用于定位实现细节)
|
||||
- 键位映射与位图更新:[client/src/inputdevice.js:287-307](file://client/src/inputdevice.js#L287-L307)
|
||||
- 文本事件生成:[client/src/inputdevice.js:639-646](file://client/src/inputdevice.js#L639-L646)
|
||||
- 重复事件过滤与文本事件生成:[client/src/sender.js:130-143](file://client/src/sender.js#L130-L143)
|
||||
- 状态事件打包:[client/src/inputremoting.js:258-277](file://client/src/inputremoting.js#L258-L277)
|
||||
- 应用层修饰键快捷键示例:[client/public/onebyone/main.js:140-144](file://client/public/onebyone/main.js#L140-L144)
|
||||
|
||||
[本节为参考索引,无需具体文件引用]
|
||||
Reference in New Issue
Block a user