# 视频播放器示例

<cite>
**本文档引用的文件**
- [video-player.js](file://client/public/videoplayer/js/video-player.js)
- [gamepadEvents.js](file://client/public/videoplayer/js/gamepadEvents.js)
- [register-events.js](file://client/public/videoplayer/js/register-events.js)
- [main.js](file://client/public/videoplayer/js/main.js)
- [index.html](file://client/public/videoplayer/index.html)
- [peer.js](file://client/src/peer.js)
- [signaling.js](file://client/src/signaling.js)
- [renderstreaming.js](file://client/src/renderstreaming.js)
- [sender.js](file://client/src/sender.js)
- [inputdevice.js](file://client/src/inputdevice.js)
- [inputremoting.js](file://client/src/inputremoting.js)
- [logger.js](file://client/src/logger.js)
- [keymap.js](file://client/src/keymap.js)
- [mousebutton.js](file://client/src/mousebutton.js)
- [gamepadbutton.js](file://client/src/gamepadbutton.js)
</cite>

## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考虑](#性能考虑)
8. [故障排除指南](#故障排除指南)
9. [结论](#结论)
10. [附录](#附录)

## 简介
本示例演示了如何在浏览器中接收 Unity 渲染的相机图像，并通过浏览器端操作 Unity 中的相机。系统采用 WebRTC 进行媒体流传输，同时通过数据通道发送用户输入事件（键盘、鼠标、触摸、游戏手柄）。播放器支持主画中画双路视频切换、缩放与坐标转换、以及多种输入设备的实时控制。

## 项目结构
视频播放器示例位于 Public 模式下的 videoplayer 子目录，主要由以下层次组成：
- 视频播放层：负责建立 WebRTC 连接、接收媒体轨道、管理数据通道、处理来自 Unity 的控制消息。
- 事件注册层：封装各类输入事件的监听与打包，统一通过数据通道发送至 Unity。
- WebRTC 基础层：封装 PeerConnection、信令协议、ICE 候选等底层细节。
- 输入设备抽象层：定义输入设备、状态与事件的数据格式，便于序列化与跨平台传输。

```mermaid
graph TB
subgraph "浏览器前端"
VP["VideoPlayer<br/>视频播放器"]
RE["register-events.js<br/>事件注册"]
GP["gamepadEvents.js<br/>游戏手柄事件"]
PEER["Peer<br/>RTCPeerConnection封装"]
SIG["Signaling/WebSocketSignaling<br/>信令"]
end
subgraph "Unity 后端"
RS["RenderStreaming<br/>渲染流封装"]
SENDER["Sender<br/>输入发送器"]
INPUTDEV["InputDevice/IInputState<br/>输入设备与状态"]
end
VP --> PEER
VP --> SIG
RE --> VP
GP --> RE
PEER --> SIG
RS --> PEER
SENDER --> RS
INPUTDEV --> SENDER
```

**图表来源**
- [video-player.js:18-247](file://client/public/videoplayer/js/video-player.js#L18-L247)
- [register-events.js:1-308](file://client/public/videoplayer/js/register-events.js#L1-L308)
- [gamepadEvents.js:1-147](file://client/public/videoplayer/js/gamepadEvents.js#L1-L147)
- [peer.js:1-188](file://client/src/peer.js#L1-L188)
- [signaling.js:1-292](file://client/src/signaling.js#L1-L292)
- [renderstreaming.js:1-317](file://client/src/renderstreaming.js#L1-L317)
- [sender.js:1-209](file://client/src/sender.js#L1-L209)
- [inputdevice.js:1-719](file://client/src/inputdevice.js#L1-L719)

**章节来源**
- [index.html:1-38](file://client/public/videoplayer/index.html#L1-L38)
- [main.js:1-157](file://client/public/videoplayer/js/main.js#L1-L157)

## 核心组件
- VideoPlayer：负责创建 RTCPeerConnection、订阅媒体轨道、管理数据通道、解析来自 Unity 的控制消息（如切换视频源）。
- register-events.js：集中注册键盘、鼠标、触摸、游戏手柄事件，将事件打包为二进制消息并通过数据通道发送。
- gamepadEvents.js：对原生游戏手柄 API 进行封装，生成统一的自定义事件（按钮按下/抬起/持续按住、轴值变化），并维护设备状态。
- Peer：封装 RTCPeerConnection 生命周期、SDP 协商、ICE 候选处理与断线检测。
- Signaling/WebSocketSignaling：HTTP 轮询或 WebSocket 实现的信令通道，承载 offer/answer/candidate/on-message 等信令。
- RenderStreaming/Sender/InputDevice：输入遥测链路的完整实现，定义输入设备、状态与事件的内存布局，支持鼠标、键盘、触摸、游戏手柄。

**章节来源**
- [video-player.js:18-247](file://client/public/videoplayer/js/video-player.js#L18-L247)
- [register-events.js:1-308](file://client/public/videoplayer/js/register-events.js#L1-L308)
- [gamepadEvents.js:1-147](file://client/public/videoplayer/js/gamepadEvents.js#L1-L147)
- [peer.js:1-188](file://client/src/peer.js#L1-L188)
- [signaling.js:1-292](file://client/src/signaling.js#L1-L292)
- [renderstreaming.js:1-317](file://client/src/renderstreaming.js#L1-L317)
- [sender.js:1-209](file://client/src/sender.js#L1-L209)
- [inputdevice.js:1-719](file://client/src/inputdevice.js#L1-L719)

## 架构总览
系统采用“浏览器播放器 + 信令 + WebRTC + 数据通道”的架构。浏览器端通过 Signaling 与服务器建立连接，协商 SDP 并交换 ICE 候选；媒体轨道通过 RTCPeerConnection 接收，数据通道用于传输输入事件。Unity 侧通过 RenderStreaming/Sender 将输入事件投递到远程渲染环境，同时可向浏览器发送控制消息（如切换视频源）。

```mermaid
sequenceDiagram
participant Browser as "浏览器"
participant VP as "VideoPlayer"
participant PEER as "Peer"
participant SIG as "Signaling/WebSocketSignaling"
participant Unity as "Unity"
Browser->>VP : 初始化并调用 setupConnection()
VP->>SIG : start() 启动信令
VP->>PEER : 创建 RTCPeerConnection
PEER->>SIG : 发送 offer
SIG-->>PEER : 下发 offer
PEER->>SIG : 发送 answer
SIG-->>PEER : 下发 answer
PEER->>SIG : 发送 ICE 候选
SIG-->>PEER : 下发 ICE 候选
PEER-->>VP : 触发 ontrack 获取媒体轨道
VP->>VP : 切换主/副视频轨道
Unity-->>VP : 数据通道下发控制消息
VP->>VP : 解析消息并执行操作
```

**图表来源**
- [video-player.js:49-155](file://client/public/videoplayer/js/video-player.js#L49-L155)
- [peer.js:57-173](file://client/src/peer.js#L57-L173)
- [signaling.js:30-149](file://client/src/signaling.js#L30-L149)

## 详细组件分析

### VideoPlayer 组件分析
VideoPlayer 是播放器的核心，负责：
- 建立 RTCPeerConnection 并绑定事件（trackevent、sendoffer、sendanswer、sendcandidate）。
- 订阅媒体轨道，维护主/副视频轨道列表，支持动态切换。
- 创建数据通道，接收来自 Unity 的控制消息并执行相应操作（如切换视频源）。
- 提供尺寸与坐标转换能力，确保输入事件映射到正确的视频区域。

```mermaid
classDiagram
class VideoPlayer {
+constructor(elements)
+setupConnection(useWebSocket)
+resizeVideo()
+switchVideo(indexVideoTrack)
+replaceTrack(stream, newTrack)
+sendMsg(msg)
+stop()
+ondisconnect
+videoWidth
+videoHeight
+videoOriginX
+videoOriginY
+videoScale
}
class Peer {
+addEventListener(event, handler)
+createDataChannel(connectionId, label)
+onGotDescription(connectionId, description)
+onGotCandidate(connectionId, candidate)
+close()
}
class Signaling {
+start()
+sendOffer(connectionId, sdp)
+sendAnswer(connectionId, sdp)
+sendCandidate(connectionId, candidate, sdpMid, sdpMLineIndex)
+addEventListener(event, handler)
+stop()
}
VideoPlayer --> Peer : "使用"
VideoPlayer --> Signaling : "使用"
```

**图表来源**
- [video-player.js:18-247](file://client/public/videoplayer/js/video-player.js#L18-L247)
- [peer.js:1-188](file://client/src/peer.js#L1-L188)
- [signaling.js:1-292](file://client/src/signaling.js#L1-L292)

**章节来源**
- [video-player.js:18-247](file://client/public/videoplayer/js/video-player.js#L18-L247)

### register-events.js 事件注册与处理
该模块负责：
- 键盘事件：按键按下/抬起，映射键码并发送二进制消息。
- 鼠标事件：点击/移动/滚轮，计算相对于视频的坐标并发送。
- 触摸事件：多点触控，记录每个触点的阶段、坐标、压力等信息。
- 游戏手柄事件：统一的按钮与轴事件，生成自定义事件并转发给 VideoPlayer。

```mermaid
flowchart TD
Start(["开始"]) --> Detect["检测输入事件类型"]
Detect --> |键盘| BuildKey["构建键盘消息<br/>类型/方向/重复/键码/字符"]
Detect --> |鼠标| BuildMouse["构建鼠标消息<br/>坐标/按钮"]
Detect --> |触摸| BuildTouch["构建触摸消息<br/>触点数量/阶段/坐标/压力"]
Detect --> |游戏手柄| BuildGP["构建手柄消息<br/>按钮/轴值"]
BuildKey --> Send["通过数据通道发送"]
BuildMouse --> Send
BuildTouch --> Send
BuildGP --> Send
Send --> End(["结束"])
```

**图表来源**
- [register-events.js:120-281](file://client/public/videoplayer/js/register-events.js#L120-L281)

**章节来源**
- [register-events.js:1-308](file://client/public/videoplayer/js/register-events.js#L1-L308)

### gamepadEvents.js 游戏手柄事件处理
该模块对原生游戏手柄 API 进行封装：
- 维护设备连接时间戳与前一帧状态，避免重复触发。
- 定时循环（约 60fps）扫描按钮与轴值变化，生成统一的自定义事件。
- 支持不同浏览器差异（如 getGamepads 的兼容性）。

```mermaid
sequenceDiagram
participant Browser as "浏览器"
participant GPH as "gamepadEvents.js"
participant RE as "register-events.js"
participant VP as "VideoPlayer"
Browser->>GPH : 设备连接/断开
GPH->>GPH : 记录连接时间戳/初始化状态
loop 60fps
GPH->>GPH : 扫描按钮/轴值变化
alt 按钮状态变化
GPH-->>RE : 触发 gamepadButtonDown/gamepadButtonUp
else 按钮持续按住
GPH-->>RE : 触发 gamepadButtonPressed
end
alt 轴值变化
GPH-->>RE : 触发 gamepadAxis
end
end
RE->>VP : sendMsg(二进制消息)
```

**图表来源**
- [gamepadEvents.js:68-94](file://client/public/videoplayer/js/gamepadEvents.js#L68-L94)
- [register-events.js:42-101](file://client/public/videoplayer/js/register-events.js#L42-L101)
- [video-player.js:215-233](file://client/public/videoplayer/js/video-player.js#L215-L233)

**章节来源**
- [gamepadEvents.js:1-147](file://client/public/videoplayer/js/gamepadEvents.js#L1-L147)
- [register-events.js:42-101](file://client/public/videoplayer/js/register-events.js#L42-L101)

### 输入设备抽象与序列化
输入设备层定义了统一的状态与事件格式，便于跨平台传输：
- InputDevice：抽象基类，维护设备描述与当前状态。
- IInputState：抽象状态接口，提供 buffer 与 format 字段。
- 具体设备状态：MouseState、KeyboardState、TouchscreenState、GamepadState。
- 事件封装：StateEvent、TextEvent，支持二进制序列化。

```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 StateEvent
class TextEvent
InputDevice <|-- Mouse
InputDevice <|-- Keyboard
InputDevice <|-- Touchscreen
InputDevice <|-- Gamepad
IInputState <|-- MouseState
IInputState <|-- KeyboardState
IInputState <|-- TouchscreenState
IInputState <|-- GamepadState
StateEvent --> IInputState : "包含状态"
TextEvent --> InputEvent : "包含基础事件"
```

**图表来源**
- [inputdevice.js:40-719](file://client/src/inputdevice.js#L40-L719)

**章节来源**
- [inputdevice.js:1-719](file://client/src/inputdevice.js#L1-L719)

### 信令与 WebRTC 协商
- Signaling：基于 HTTP 的轮询信令，支持 offer/answer/candidate/on-message。
- WebSocketSignaling：基于 WebSocket 的实时信令，支持广播与参与者事件。
- Peer：封装 RTCPeerConnection，自动处理 SDP 协商、ICE 候选、断线检测与重发 offer。

```mermaid
sequenceDiagram
participant A as "Peer A"
participant SIG as "Signaling"
participant B as "Peer B"
A->>SIG : 发送 offer
SIG-->>B : 下发 offer
B->>B : 设置远端 offer
B->>SIG : 发送 answer
SIG-->>A : 下发 answer
A->>A : 设置远端 answer
A->>SIG : 发送 ICE 候选
B->>SIG : 发送 ICE 候选
SIG-->>A : 下发 ICE 候选
SIG-->>B : 下发 ICE 候选
```

**图表来源**
- [peer.js:57-173](file://client/src/peer.js#L57-L173)
- [signaling.js:117-146](file://client/src/signaling.js#L117-L146)

**章节来源**
- [peer.js:1-188](file://client/src/peer.js#L1-L188)
- [signaling.js:1-292](file://client/src/signaling.js#L1-L292)

## 依赖关系分析
- VideoPlayer 依赖 Peer 与 Signaling，用于媒体与信令。
- register-events 依赖 gamepadEvents 与 keymap，用于事件注册与键码映射。
- main.js 作为入口，协调 VideoPlayer 与事件注册模块。
- 输入遥测链路由 Sender -> RenderStreaming -> Peer -> Signaling，最终到达 Unity。

```mermaid
graph LR
MAIN["main.js"] --> VP["video-player.js"]
MAIN --> REG["register-events.js"]
REG --> GPE["gamepadEvents.js"]
VP --> PEER["peer.js"]
VP --> SIG["signaling.js"]
SENDER["sender.js"] --> RS["renderstreaming.js"]
RS --> PEER
INPUTDEV["inputdevice.js"] --> SENDER
```

**图表来源**
- [main.js:1-157](file://client/public/videoplayer/js/main.js#L1-L157)
- [video-player.js:1-247](file://client/public/videoplayer/js/video-player.js#L1-L247)
- [register-events.js:1-308](file://client/public/videoplayer/js/register-events.js#L1-L308)
- [gamepadEvents.js:1-147](file://client/public/videoplayer/js/gamepadEvents.js#L1-L147)
- [peer.js:1-188](file://client/src/peer.js#L1-L188)
- [signaling.js:1-292](file://client/src/signaling.js#L1-L292)
- [sender.js:1-209](file://client/src/sender.js#L1-L209)
- [renderstreaming.js:1-317](file://client/src/renderstreaming.js#L1-L317)
- [inputdevice.js:1-719](file://client/src/inputdevice.js#L1-L719)

**章节来源**
- [main.js:1-157](file://client/public/videoplayer/js/main.js#L1-L157)

## 性能考虑
- 事件采样频率：游戏手柄事件以约 60fps 轮询，建议根据设备刷新率调整间隔，避免过度 CPU 占用。
- 坐标转换：VideoPlayer 提供视频缩放与偏移计算，确保输入事件映射准确，减少误触。
- 数据通道：仅在通道打开时发送消息，避免在关闭或连接中发送导致异常。
- 媒体轨道切换：使用 replaceTrack 替换视频轨道，避免频繁创建/销毁媒体流带来的抖动。

## 故障排除指南
- 无法建立 WebRTC 连接
  - 检查信令是否正常启动与停止。
  - 确认 offer/answer 协商过程无异常，ICE 候选是否正确交换。
  - 查看断线事件与日志输出。
- 数据通道不可用
  - 确认数据通道已创建且处于 open 状态。
  - 检查 sendMsg 的状态分支，避免在 closing/closed 状态发送。
- 输入事件未生效
  - 确认事件监听已注册，且消息格式与 Unity 期望一致。
  - 检查坐标转换参数（videoScale、videoOriginX/Y）是否正确。
- 游戏手柄无响应
  - 确认浏览器支持 getGamepads 或 webkitGetGamepads。
  - 检查设备连接时间戳与状态缓存是否正确初始化。

**章节来源**
- [video-player.js:128-154](file://client/public/videoplayer/js/video-player.js#L128-L154)
- [register-events.js:237-263](file://client/public/videoplayer/js/register-events.js#L237-L263)
- [gamepadEvents.js:112-146](file://client/public/videoplayer/js/gamepadEvents.js#L112-L146)
- [logger.js:1-30](file://client/src/logger.js#L1-L30)

## 结论
该视频播放器示例完整展示了从浏览器接收 Unity 渲染视频、通过数据通道发送用户输入、以及在 Unity 中进行相机控制的端到端流程。其模块化设计使得事件注册、输入抽象与 WebRTC 协商清晰分离，便于扩展与维护。通过合理的坐标转换与事件采样策略，系统在多设备环境下具备良好的交互体验。

## 附录

### 使用指南
- Public 模式配置
  - 通过配置接口获取服务器设置，决定使用 HTTP 轮询信令还是 WebSocket 信令。
  - 若启动模式为私有模式，页面会显示警告提示。
- 启动播放器
  - 点击播放按钮后，页面动态创建主视频与缩略视频元素。
  - 初始化 VideoPlayer 并建立 WebRTC 连接。
- 控制 Unity 中的相机
  - 通过数据通道发送控制消息（如切换视频源）。
  - 可通过按钮事件向 Unity 发送按钮点击消息。
- 输入设备使用
  - 键盘：按键按下/抬起事件会被打包发送。
  - 鼠标：点击/移动/滚轮事件，坐标经视频缩放与偏移转换。
  - 触摸：多点触控，记录触点阶段、坐标、压力等。
  - 游戏手柄：按钮与轴值变化统一事件，支持不同浏览器差异。
- 设备适配
  - 坐标系适配：VideoPlayer 提供缩放与偏移计算，确保事件映射到正确区域。
  - 全屏模式：支持全屏切换，改变按钮可见性与布局。
  - 断线处理：断线时清理 DOM、停止播放器并重新显示播放按钮。

**章节来源**
- [main.js:23-157](file://client/public/videoplayer/js/main.js#L23-L157)
- [video-player.js:49-247](file://client/public/videoplayer/js/video-player.js#L49-L247)
- [register-events.js:120-308](file://client/public/videoplayer/js/register-events.js#L120-L308)
- [gamepadEvents.js:112-147](file://client/public/videoplayer/js/gamepadEvents.js#L112-L147)