云渲染
WebRTC 本身提供的是 1 对 1 的通信模型,在 STUN/TURN 的辅助下,如果能实现 NAT 穿越,那么两个浏览器是可以直接进行媒体数据交换的;如果不能实现 NAT 穿越,那么只能通过 TURN 服务器进行数据转发的方式实现通信。目前来看,Google 开源的用于学习和研究的项目基本都是基于 STUN/TURN 的 1 对 1 通信。
Mesh 方案,即多个终端之间两两进行连接,形成一个网状结构。比如 A、B、C 三个终端进行多对多通信,当 A 想要共享媒体(比如音频、视频)时,它需要分别向 B 和 C 发送数据。同样的道理,B 想要共享媒体,就需要分别向 A、C 发送数据,依次类推。这种方案对各终端的带宽要求比较高。
MCU(Multipoint Conferencing Unit)方案,该方案由一个服务器和多个终端组成一个星形结构。各终端将自己要共享的音视频流发送给服务器,服务器端会将在同一个房间中的所有终端的音视频流进行混合,最终生成一个混合后的音视频流再发给各个终端,这样各终端就可以看到 / 听到其他终端的音视频了。实际上服务器端就是一个音视频混合器,这种方案服务器的压力会非常大。
SFU(Selective Forwarding Unit)方案,该方案也是由一个服务器和多个终端组成,但与 MCU 不同的是,SFU 不对音视频进行混流,收到某个终端共享的音视频流后,就直接将该音视频流转发给房间内的其他终端。它实际上就是一个音视频路由转发器。
那 MCU 的优势有哪些呢?大致可总结为如下几点:
- 技术非常成熟,在硬件视频会议中应用非常广泛。
- 作为音视频网关,通过解码、再编码可以屏蔽不同编解码设备的差异化,满足更多客户的集成需求,提升用户体验和产品竞争力。
- 将多路视频混合成一路,所有参与人看到的是相同的画面,客户体验非常好。
同样,MCU 也有一些不足,主要表现为:
- 重新解码、编码、混流,需要大量的运算,对 CPU 资源的消耗很大。
- 重新解码、编码、混流还会带来延迟。
- 由于机器资源耗费很大,所以 MCU 所提供的容量有限,一般十几路视频就是上限了。
游戏领域云渲染
考虑将高负载的“渲染”放置到能力强大的后端,解放前端, 为此一些游戏引擎厂商提出了一种新的三维“云渲染”方案-基于WebRTC的视频流推送技术:
- UE称之为 PixelStreaming;
- Unity称之为 RenderStreaming;
由于基本原理一致,就是将场景通过后端渲染,然后采用实时视频流推送到网页端,并且能够实现前端到后端的交互同步。通过 WebRTC 协 议将其发送给位于接收端的浏览器和设备。事实上,通过在高性能主机系统上运行渲染引擎,用户能在所有终 端设备上享受到与主机相同的画质,并且能体验到所有的渲染引擎功能。
WebRTC(网页实时通信)是一种通过网页浏览器和移动应用程序进行实时通信的协议。该协议允许以直接链接 的方式传输音频和视频,用户无需下载任何插件或应用程序。通信命令通过 API 接口提交,前端只要声明一个 video 标签就可以实现视频流的加载和交互。
Unreal
UE5云渲染方案叫PixelStreaming
-
UE启用Pixel Streaming插件
-
下载PixelStreamingInfrastucture源码,执行setup.bat下载依赖,本地会自动生成 nexe 和 coturn 文件夹。 node是nodejs本地执行环境, 如果不想在脚本下载, 可以直接copy过来, 注意版本号对齐, 默认是:v16.4.2
- 执行
Start_SignallingServer
启动服务器
-
启动UE打包的客户端,启动参数传
-AudioMixer -PixelStreamingURL=ws://localhost:8888 -RenderOffScreen
-
连接服务器,网页输入127.0.0.1:8080测试,
当有客户端连接时, 会有如下提示:
配置文件位于SignallingWebServer的Config.json下,配置内容如下, 这里可以修改启动的服务器的端口:
{
"UseFrontend": false,
"UseMatchmaker": false,
"UseHTTPS": false,
"UseAuthentication": false,
"LogToFile": true,
"LogVerbose": true,
"HomepageFile": "player.html",
"AdditionalRoutes": {},
"EnableWebserver": true,
"MatchmakerAddress": "",
"MatchmakerPort": "9999",
"PublicIp": "localhost",
"HttpPort": 8080,
"HttpsPort": 443,
"StreamerPort": 8888,
"SFUPort": 8889,
"MaxPlayerCount": -1
}
浏览器启来之后效果如下:
Unity
具体的消息传递过程:
其中offer的调用过程可以参考:
发起方的信令变化:
have-local-offer -> stable
被叫方的信令变化
have-remote-offer -> stable
singaling 状态:
当我们一开始创建这个RTCPeerConnection的时候,它处于stable状态,就是处于一个稳定状态,这个时候实际connection就可以用了,但用的时候它是不能进行这个编解码的,为什么呢?
因为他没有进行数据协商对吧,虽然我这个connection类是可以用,但是并没有进行数据协商,所以他没法儿进行数据的传输与编解码,怎么才能进行数据传输编解码呢?
那就发生了一个状态的改变,就是比如对于调用者来说,首先创建了connection之后他要创建这个offer,创建offer之后, 通过调用那个setLocalDescription将这个offer设进去之后; 他就状态变化了,变成什么呢,变成have-local-offer,但是我设完这个之后, 如果对方没有给我回他的answer的时候, 那实际我的状态就一直处于have-local-offer,无论我在接受多少次这个setLocalDescription方法仍在处理这个状态,所以他自己对自己的一个循环对我仍然处于这个状态,那这个状态是不会变的,那什么时候才会变呢?
只有在你远端的answer回来的时候,像我刚才讲的远端的answer创建好,然后通过消息传给这个调用者的时候,那它才会调用这个setRemoteDescription,那么将answer设进去之后,他又回到了stable状态,这个时候RTCpeerConnection又可以用了,而且是已经协商过的了。
这时候他可以进行编解码和传输了,这是对于调用者来说。
那么对于这个被调用者来说呢,同样道理,那当他收到这个offer之后呢,它要调用setRemote offer,这个时候,他从那个stable状态就变成了have-remote-offer,那同样的,当他自己创建了一个answer之后,并且调用了setLocalDescription这个方法将answer设置进去之后,他又从这个remote-offer变成了stable状态,那这个时候他又可以工作了。
如上图所示,媒体协商状态变化除了stable状态外,还有have-local-offer以及have-remote-offer:
首先在创建offer之后呢,会调用setLocalDescription将这个offer设置进去,那他的状态呢,就变成了have-local-offer,那当他收到对端的这个answer之后呢,它会调用setRemoteDescription将这个offer设置进去,这样就完成了一个协商,所以他就从这个have-local-offer变为了stable状态,那他就可以继续下面的工作了,而对于被调用者,他首先呢是从信令服务器收到一个offer,那他首先调用setRemoteDescription这个offer,那它就变成了have-remote-offer状态,这个时候,他在调用自己的这个create answer, 创建完自己的这个answer之后, 它调用setLocalDescription answer就从这个have-remote-offer变为了stable状态,这样的被调用者他也就完成了自己的协商工作,可以继续下面的这个操作了,但是还是两种情况,会有一种中间的这个状态叫做PRanswer,就是提前应答,这个状态是什么时候会产生呢?
就是在双方通讯的时候其中被调用者还没有准备好数据的时候,那可以先创建一个临时的这个answer,这个临时的answer有一个特点就是: 它没有媒体数据也就是说没有音频流和视频流,并且将这个发送的方向设置成send only
对于B来说,他回的这个answer是一个什么样的answer呢 ?
就是说,我的媒体流还没有准备好,所以就没有媒体流,但是我呢,只能发送,不能接受,当他发给对方A的时候,A收到这样一个send only,他就知道,对方还不能进入数据,所以这时候他们的通讯虽然是做了的协商,但是他们之间还不能进行通讯。
因为第一个是对方没有媒体流,第二个是对方不接受我的数据。处于这样一个状态有什么好处呢?
那就是可以提前建立这个链路的连接,也就是说包括ICE,包括这个DLS这些跟链路相关的这个协商其实都已经创建好了,对刚才我们已经介绍了,就是对于B来说,他已经提前准备好了一个answer,但这个answer里有没有媒体数据,但是实际是有网络数据的,我收集的各种各种候选者实际都已经有了。
那么就可以提前交给这个A,那A与B之间,实际就是链路层已经协商好了,包括这个DLS还要进行这个握手,它因为是安全加密,加密所以要进行握手,握手的时间其实还是蛮长的,那在B准备好这个自己的流之前,将所有的链路都准备好,那一旦这个B向那个用户申请:说想开启音频和视频,当用户授权说可以,这个时候呢,他们拿到数据之后,只要将数据传进去,就可以进行这个通讯了。
所以在B没有准备好之前,他可以使用一个PRanswer,就是提前预定好的一个answer给这个A发过去,发过去之后呢,它就变成了这个have-remote-offer这个状态,这是一个中间状态,
在这个状态下,双方的这个链路是可以协商好的,只是没有这个媒体数据,当那个B设置好他自己的媒体流之后,就是一切都准备好之后,然后再给他回一个最终的answer,当调用者收到它这个最终的answer之后,他又变成了stable状态,那双方就可以就真正协商好了。这时候,实际是减少了底层的这个网络流的这个握手,以及一些其他的逻辑处理工作,这样就节省了时间。但对于被调用者也是类似的,所以在他回这个真正的answer之前,它是处于这have-local-PRanswer的,当真正的这个最终的Answer,准备好之后呢,再重新设一下setLocalAnswer,他又变成了stable状态,这就是一个整个协商完整的一个状态变化。只有在整个协商完成之后,才能进行我们后边儿的真正的音视频数据的传输以及编解码。这就是协商状态的变化。
同时 ICE connection 和 ICE gathering 状态的变化:
ICE gathering state: new -> gathering -> complete
ICE connection state: new -> checking -> connected -> disconnected
如果双方同时在本地生成了offer, 然后同时发给了对方, 肯定会协商失败
Unity 自行编译 Server
编译webapp 执行如下:
cd WebApp
npm install
npm run build
npm run start
使用 ts-node 运行server:
npm run dev
打包发布:
npm run pack
npm run之类的命令, 实际执行的 package.json里的配置, 比如 build 执行的是:
tsc -p tsconfig.build.json
start执行的是:
node ./build/index.js
Typescript 环境
Typescript 在webstorm 中调试环境配置
大体上来看3大步:
- 安装ts-node
- 为ts-node创建一个自定义Node.js运行/调试配置
安装 ts_node, 直接在终端上执行下面的命名
npm install ts-node
创建 运行调试配置:
运行查看中间过程:
QA
1. 关于 polite peer
-
A polite peer, 如果将要设置的信令与本身的信令状态有冲突, 就会使用回滚, 使用自身的信令状态变为stable后继续设置对方发来的信令
-
A impolite peer, 如果将设置的信令与本身信令状态有冲突, 就会放弃设置对方的信令(以我为准)
双方谁是 polite peer 和 impolite peer 没有限制, 是可以随机的。
2. 关于 connectionId
connectionId 在 renderstreaming.js 的 createConnection 方法里, 使用 uuid4() 创建。
function uuid4() {
var temp_url = URL.createObjectURL(new Blob());
var uuid = temp_url.toString();
URL.revokeObjectURL(temp_url);
return uuid.split(/[:/]/g).pop().toLowerCase(); // remove prefixes
}
3. 关于 candidate 的生成
candidate 信息(candidate、sdpMLineIndex、sdpMid) 是在 peer.js 里 RTCPeerConnection的onicecandidate 触发, 可以参考:https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/icecandidate_event
4. unity的 multiPlay 多人场景
实现基于 一个scene里包含多个camera的方案, 每个链接对应一个camera, 各自渲染自己的画面和响应交互。 没有像MCU 那样需要单独的服务器给各个端进行混合。