# 多播放示例

<cite>
**本文引用的文件**
- [client/src/renderstreaming.js](file://client/src/renderstreaming.js)
- [client/src/signaling.js](file://client/src/signaling.js)
- [client/src/peer.js](file://client/src/peer.js)
- [client/public/multiplay/js/main.js](file://client/public/multiplay/js/main.js)
- [src/index.ts](file://src/index.ts)
- [src/server.ts](file://src/server.ts)
- [src/websocket.ts](file://src/websocket.ts)
- [src/class/websockethandler.ts](file://src/class/websockethandler.ts)
- [src/signaling.ts](file://src/signaling.ts)
- [src/class/httphandler.ts](file://src/class/httphandler.ts)
- [package.json](file://package.json)
</cite>

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

## 简介
本示例演示如何在浏览器端与服务器之间构建多播式媒体分发通道，使多个客户端能够同时接收来自同一信源的相同媒体流。该实现以 WebRTC 为核心，结合 WebSocket 或 HTTP 轮询信令通道，支持：
- 多客户端同时加入同一连接组
- 服务器侧按连接组进行消息路由与广播
- 客户端侧根据角色（主机/参与者）分别管理独立的 PeerConnection
- 基于数据通道的广播消息与输入事件传递

本指南聚焦于多播场景下的连接管理、媒体流复制与同步机制，并给出在 Unity Render Streaming 的 Multiplay 场景中的使用方法、集成步骤、性能优化与故障处理建议。

## 项目结构
该仓库采用前后端分离结构：
- 前端示例位于 client/public/multiplay，包含多客户端多播播放器与输入通道
- 前端核心逻辑位于 client/src，封装了信令、WebRTC Peer 管理与渲染
- 服务端位于 src，提供 HTTP 服务与 WebSocket 信令服务，以及信令路由与广播

```mermaid
graph TB
subgraph "前端"
FE_Main["multiplay/main.js"]
FE_Render["renderstreaming.js"]
FE_Signaling["signaling.js"]
FE_Peer["peer.js"]
end
subgraph "服务端"
BE_Index["index.ts"]
BE_Server["server.ts"]
BE_WS["websocket.ts"]
BE_WS_Handler["class/websockethandler.ts"]
BE_HTTP_Router["signaling.ts"]
BE_HTTP_Handler["class/httphandler.ts"]
end
FE_Main --> FE_Render
FE_Render --> FE_Signaling
FE_Render --> FE_Peer
FE_Signaling --> BE_Index
BE_Index --> BE_Server
BE_Server --> BE_WS
BE_WS --> BE_WS_Handler
BE_Server --> BE_HTTP_Router
BE_HTTP_Router --> BE_HTTP_Handler
```

**图表来源**
- [client/public/multiplay/js/main.js:1-204](file://client/public/multiplay/js/main.js#L1-L204)
- [client/src/renderstreaming.js:1-317](file://client/src/renderstreaming.js#L1-L317)
- [client/src/signaling.js:1-292](file://client/src/signaling.js#L1-L292)
- [client/src/peer.js:1-188](file://client/src/peer.js#L1-L188)
- [src/index.ts:1-109](file://src/index.ts#L1-L109)
- [src/server.ts:1-90](file://src/server.ts#L1-L90)
- [src/websocket.ts:1-118](file://src/websocket.ts#L1-L118)
- [src/class/websockethandler.ts:1-479](file://src/class/websockethandler.ts#L1-L479)
- [src/signaling.ts:1-25](file://src/signaling.ts#L1-L25)
- [src/class/httphandler.ts:1-800](file://src/class/httphandler.ts#L1-L800)

**章节来源**
- [src/server.ts:14-90](file://src/server.ts#L14-L90)
- [src/index.ts:52-109](file://src/index.ts#L52-L109)

## 核心组件
- 多播渲染层（RenderStreaming）
  - 负责根据角色（主机/参与者）管理多个 PeerConnection，路由信令与媒体事件
  - 提供创建/删除连接、创建数据通道、添加轨道、获取统计等能力
- 信令层（Signaling/ WebSocketSignaling）
  - 支持 HTTP 轮询与 WebSocket 两种信令协议
  - 负责连接生命周期管理、offer/answer/candidate 的收发与广播
- Peer 层（Peer）
  - 封装 RTCPeerConnection，处理 SDP 协商、ICE 候选、数据通道与统计
- 服务器信令处理器（WebSocket/HTTP）
  - 维护连接组（主机 + 多个参与者），实现消息路由与广播
  - 支持私有模式（一对一）与公共模式（一对多）

**章节来源**
- [client/src/renderstreaming.js:11-317](file://client/src/renderstreaming.js#L11-L317)
- [client/src/signaling.js:3-292](file://client/src/signaling.js#L3-L292)
- [client/src/peer.js:3-188](file://client/src/peer.js#L3-L188)
- [src/class/websockethandler.ts:145-260](file://src/class/websockethandler.ts#L145-L260)
- [src/class/httphandler.ts:739-800](file://src/class/httphandler.ts#L739-L800)

## 架构总览
多播架构的关键在于“连接组”与“角色路由”。服务器为每个 connectionId 维护一个连接组，首个加入者作为主机，后续加入者作为参与者。主机与参与者之间的信令消息按角色进行定向转发；广播消息则在组内进行全量分发。

```mermaid
sequenceDiagram
participant C1 as "客户端1主机"
participant C2 as "客户端2参与者"
participant Cn as "客户端n参与者"
participant WS as "WebSocket服务器"
participant H as "连接组处理器"
C1->>WS : "connect(connectionId)"
WS->>H : "onConnect(connectionId)"
H-->>C1 : "connect(role=host, participantId=p1)"
C2->>WS : "connect(connectionId)"
WS->>H : "onConnect(connectionId)"
H-->>C2 : "connect(role=participant, participantId=p2)"
H-->>C1 : "participant-joined(p2)"
C2->>WS : "offer(sdp, from=p2)"
WS->>H : "onOffer(data, from=p2)"
H-->>C1 : "offer(sdp, from=p2, participantId=p2)"
C1->>WS : "answer(sdp, from=p1)"
WS->>H : "onAnswer(data, from=p1)"
H-->>C2 : "answer(sdp, from=p1, participantId=p2)"
C1->>WS : "candidate(...)"
C2->>WS : "candidate(...)"
WS->>H : "onCandidate(...)"
H-->>C2 : "candidate(..., participantId=p2)"
H-->>C1 : "candidate(...)"
```

**图表来源**
- [src/websocket.ts:44-115](file://src/websocket.ts#L44-L115)
- [src/class/websockethandler.ts:145-338](file://src/class/websockethandler.ts#L145-L338)

**章节来源**
- [src/websocket.ts:15-118](file://src/websocket.ts#L15-L118)
- [src/class/websockethandler.ts:145-338](file://src/class/websockethandler.ts#L145-L338)

## 详细组件分析

### 多客户端连接管理与媒体流复制
- 连接组与角色
  - 首个连接者为主机，后续连接者为参与者
  - 服务器为每个 connectionId 维护 host 与 participants 集合
- 多 Peer 管理
  - 主机端为每个参与者维护独立的 Peer 实例，确保多路媒体复制与独立统计
  - 参与者端仅维护单一 Peer，避免不必要的连接开销
- 媒体复制与同步
  - 服务器对 offer/answer/candidate 进行定向转发或广播
  - 客户端在收到 offer 时创建/复用对应参与者的 Peer 并设置 SDP，随后自动生成 answer 并回传
  - ICE 候选同样按目标参与者路由，保证连接建立的正确性

```mermaid
flowchart TD
Start(["开始：收到offer"]) --> IsHost{"是否为主机？"}
IsHost --> |是| GetPeer["获取/创建目标参与者的Peer"]
IsHost --> |否| CreatePeer["创建或复用单一Peer"]
GetPeer --> SetSDP["设置远程SDPoffer"]
CreatePeer --> SetSDP
SetSDP --> GenAnswer["生成本地SDPanswer"]
GenAnswer --> SendAnswer["发送answer给目标方"]
SendAnswer --> End(["结束"])
```

**图表来源**
- [client/src/renderstreaming.js:72-130](file://client/src/renderstreaming.js#L72-L130)
- [client/src/peer.js:132-173](file://client/src/peer.js#L132-L173)

**章节来源**
- [client/src/renderstreaming.js:72-130](file://client/src/renderstreaming.js#L72-L130)
- [client/src/peer.js:132-173](file://client/src/peer.js#L132-L173)

### 服务器端多播路由与广播
- 连接组维护
  - 使用 Map<connectionId, { host, participants }> 维护连接组
  - 通过 participantId 标识参与者，实现按参与者定向转发
- 消息路由
  - offer/answer/candidate：主机发给指定参与者，参与者发给主机
  - broadcast：服务器向组内所有成员广播消息
- 断线与清理
  - 主机断线时，向所有参与者广播断线通知并清理连接组
  - 参与者断线时，通知主机并从组内移除

```mermaid
flowchart TD
A["收到消息"] --> Type{"消息类型？"}
Type --> |offer/answer/candidate| Route["按角色与participantId路由"]
Type --> |broadcast| Broad["向组内广播"]
Route --> Send["发送到目标WS"]
Broad --> Send
Send --> Done["完成"]
```

**图表来源**
- [src/class/websockethandler.ts:214-338](file://src/class/websockethandler.ts#L214-L338)
- [src/class/websockethandler.ts:370-402](file://src/class/websockethandler.ts#L370-L402)

**章节来源**
- [src/class/websockethandler.ts:145-206](file://src/class/websockethandler.ts#L145-L206)
- [src/class/websockethandler.ts:214-338](file://src/class/websockethandler.ts#L214-L338)
- [src/class/websockethandler.ts:370-402](file://src/class/websockethandler.ts#L370-L402)

### 客户端渲染与输入通道（Multiplay 示例）
- 页面初始化
  - 读取服务器配置，决定使用 WebSocket 或 HTTP 信令
  - 创建 RenderStreaming 实例，注册连接、断开、轨道事件回调
- 数据通道
  - 创建名为 "multiplay" 的数据通道用于广播消息
  - 打开后随机生成标签并向服务器广播
- 媒体播放
  - 通过 onTrackEvent 回调将远端轨道接入播放器

```mermaid
sequenceDiagram
participant UI as "页面"
participant RS as "RenderStreaming"
participant Sig as "Signaling"
participant PC as "Peer"
participant VP as "VideoPlayer"
UI->>RS : "start()"
RS->>Sig : "start()"
UI->>RS : "createConnection()"
RS->>Sig : "createConnection()"
Sig-->>RS : "connect(role, participantId)"
RS->>PC : "创建/复用Peer"
RS-->>VP : "onTrackEvent -> addTrack()"
UI->>RS : "createDataChannel('multiplay')"
RS-->>UI : "onopen -> 发送标签"
```

**图表来源**
- [client/public/multiplay/js/main.js:82-125](file://client/public/multiplay/js/main.js#L82-L125)
- [client/src/renderstreaming.js:182-189](file://client/src/renderstreaming.js#L182-L189)
- [client/src/signaling.js:30-97](file://client/src/signaling.js#L30-L97)

**章节来源**
- [client/public/multiplay/js/main.js:82-125](file://client/public/multiplay/js/main.js#L82-L125)
- [client/src/renderstreaming.js:182-189](file://client/src/renderstreaming.js#L182-L189)
- [client/src/signaling.js:30-97](file://client/src/signaling.js#L30-L97)

### 服务器启动与信令服务
- 启动流程
  - 解析命令行参数，创建 Express 应用
  - 根据安全与信令类型启动 HTTP/HTTPS 与 WebSocket 服务
- 静态资源与 API
  - 提供前端静态页面与模块
  - 暴露 /config、/signaling 路由
- WebSocket 信令
  - 监听连接、消息、关闭事件
  - 分派到连接组处理器进行路由与广播

```mermaid
flowchart TD
Start(["启动"]) --> Parse["解析参数"]
Parse --> CreateApp["创建Express应用"]
CreateApp --> HTTPS{"启用HTTPS？"}
HTTPS --> |是| HTTPSrv["启动HTTPS服务器"]
HTTPS --> |否| HTTPsrv["启动HTTP服务器"]
HTTPSrv --> WS{"信令类型？"}
HTTPsrv --> WS
WS --> |websocket| WSSrv["启动WebSocket信令"]
WS --> |http| End(["完成"])
```

**图表来源**
- [src/index.ts:52-109](file://src/index.ts#L52-L109)
- [src/server.ts:14-42](file://src/server.ts#L14-L42)
- [src/websocket.ts:15-40](file://src/websocket.ts#L15-L40)

**章节来源**
- [src/index.ts:52-109](file://src/index.ts#L52-L109)
- [src/server.ts:14-42](file://src/server.ts#L14-L42)
- [src/websocket.ts:15-40](file://src/websocket.ts#L15-L40)

## 依赖关系分析
- 前端模块
  - multiplay/main.js 依赖 renderstreaming.js 与 signaling.js
  - renderstreaming.js 依赖 peer.js 与 signaling.js
  - peer.js 依赖浏览器 WebRTC API
- 服务端模块
  - index.ts 作为入口，创建 server.ts 并根据配置启动 WebSocket 信令
  - server.ts 注册 /signaling 路由，路由到 HTTP 信令处理器
  - websocket.ts 接收 WebSocket 消息，路由到连接组处理器

```mermaid
graph LR
MJS["multiplay/main.js"] --> RS["renderstreaming.js"]
RS --> SIG["signaling.js"]
RS --> PEER["peer.js"]
IDX["index.ts"] --> SRV["server.ts"]
SRV --> WSC["websocket.ts"]
SRV --> HTTR["signaling.ts"]
HTTR --> HTH["class/httphandler.ts"]
WSC --> WSH["class/websockethandler.ts"]
```

**图表来源**
- [client/public/multiplay/js/main.js:1-204](file://client/public/multiplay/js/main.js#L1-L204)
- [client/src/renderstreaming.js:1-317](file://client/src/renderstreaming.js#L1-L317)
- [client/src/signaling.js:1-292](file://client/src/signaling.js#L1-L292)
- [client/src/peer.js:1-188](file://client/src/peer.js#L1-L188)
- [src/index.ts:1-109](file://src/index.ts#L1-L109)
- [src/server.ts:1-90](file://src/server.ts#L1-L90)
- [src/websocket.ts:1-118](file://src/websocket.ts#L1-L118)
- [src/signaling.ts:1-25](file://src/signaling.ts#L1-L25)
- [src/class/httphandler.ts:1-800](file://src/class/httphandler.ts#L1-L800)
- [src/class/websockethandler.ts:1-479](file://src/class/websockethandler.ts#L1-L479)

**章节来源**
- [package.json:14-27](file://package.json#L14-L27)

## 性能考量
- 信令协议选择
  - WebSocket 适合低延迟、高并发的多播场景；HTTP 轮询开销较大但兼容性更好
- 连接组规模
  - 主机端为每个参与者维护独立 Peer，注意内存与 CPU 开销随参与者数量增长
- 媒体编解码与带宽
  - 使用 setCodecPreferences 指定编解码器，降低不必要转码成本
  - 合理设置传输参数（如 maxBitrate），避免上行拥塞
- 统计与监控
  - 定期获取 Peer 统计，观察丢包、抖动与编码速率
- 负载均衡与扩展
  - 多实例部署时，需确保同一 connectionId 的消息在同一实例内处理
  - 使用外部存储（如 Redis）共享连接组状态，实现水平扩展

[本节为通用指导，无需列出具体文件来源]

## 故障排查指南
- 无法建立连接
  - 检查服务器日志与 WebSocket/HTTP 信令是否正常
  - 确认客户端使用的信令类型与服务器配置一致
- offer/answer 循环异常
  - 查看客户端 Peer 日志，确认 SDP 设置顺序与状态机是否正确
  - 检查服务器 onOffer/onAnswer 路由是否按角色与 participantId 正确转发
- ICE 候选无法到达
  - 确认候选消息是否按目标参与者路由
  - 检查 NAT/防火墙与 STUN/TURN 配置
- 广播消息未达
  - 确认 broadcast 消息的目标 connectionId 与连接组是否存在
  - 检查客户端 on-message 事件是否正确接收

**章节来源**
- [client/src/peer.js:132-173](file://client/src/peer.js#L132-L173)
- [src/class/websockethandler.ts:214-338](file://src/class/websockethandler.ts#L214-L338)
- [src/class/websockethandler.ts:370-402](file://src/class/websockethandler.ts#L370-L402)

## 结论
本多播示例通过“连接组 + 角色路由 + 广播”的架构，实现了多客户端同时接收相同媒体流的广播式分发。前端以 RenderStreaming 为核心，配合 WebSocket/HTTP 信令与独立 Peer 管理，确保了多路媒体复制与同步；后端通过连接组处理器实现高效的消息路由与广播。结合合理的性能优化与故障排查策略，可在生产环境中稳定运行。

[本节为总结性内容，无需列出具体文件来源]

## 附录

### 在 Unity Render Streaming 的 Multiplay 场景中使用
- 场景定位
  - Multiplay 示例展示了多客户端同时观看同一渲染流的典型用法
- 关键步骤
  - 启动服务器并确认 /config 返回的 useWebSocket 与 startupMode
  - 在页面中点击“开始播放”，初始化 RenderStreaming 并创建连接
  - 通过 "multiplay" 数据通道发送标签等广播消息
- 注意事项
  - 私有模式下，页面提示不适用；请使用公共模式或调整前端逻辑
  - 若浏览器不支持 setCodecPreferences，需降级处理或提示用户

**章节来源**
- [client/public/multiplay/js/main.js:47-125](file://client/public/multiplay/js/main.js#L47-L125)
- [src/server.ts:25-29](file://src/server.ts#L25-L29)

### 服务器配置与部署
- 启动参数
  - 端口、HTTPS 证书、信令类型（websocket/http）、通信模式（public/private）、日志级别
- 部署建议
  - 使用 HTTPS 以满足现代浏览器对媒体权限的要求
  - 将静态资源与模块路径正确暴露，确保前端示例可访问
  - 对外暴露 /config、/signaling 路由，以便前端动态获取配置

**章节来源**
- [src/index.ts:20-41](file://src/index.ts#L20-L41)
- [src/server.ts:25-29](file://src/server.ts#L25-L29)
- [src/server.ts:26-28](file://src/server.ts#L26-L28)

### 客户端连接与负载均衡考虑
- 客户端连接
  - 通过 /config 动态获取信令类型与模式
  - 使用 createConnection 与 start 建立信令通道
- 负载均衡
  - 多实例部署时，确保同一 connectionId 的消息在同一实例内处理
  - 使用外部存储共享连接组状态，实现跨实例一致性

**章节来源**
- [client/public/multiplay/js/main.js:47-95](file://client/public/multiplay/js/main.js#L47-L95)
- [src/class/websockethandler.ts:145-168](file://src/class/websockethandler.ts#L145-L168)