# 多播放示例 **本文引用的文件** - [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) ## 目录 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 维护连接组 - 通过 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)