399 lines
17 KiB
Markdown
399 lines
17 KiB
Markdown
# 多播放示例
|
||
|
||
<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) |