WebRTC是一个开放的Web标准,用于支持在浏览器之间的语音、视频和通用数据的双向实时通信。在以Google为首的大厂推动下,WebRTC各项技术逐渐成熟并标准化,成为各种主流浏览器都支持的基于Web的实时音视频通信解决方案。
WebRTC本身是一个应用在客户端的类P2P技术,SRS4.0引入WebRTC处理能力,主要是为了构建服务器的SFU能力(什么是SFU读者可自行搜索)。这里借用一个网图来说明SFU的工作原理:
如上图所示,对于视频会议场景,一般都有多个WebRTC客户端。此时通过部署SFU服务器,每个WebRTC客户端都和SFU服务器之间建立一条针对本地音视频数据的推流连接,同时,WebRTC客户端和SFU服务器之间还可以按需建立多个拉流连接。这样做的好处是即利用了SFU服务器强大的客户端接入能力,又不会在服务端因为音视频混流消耗过多的CPU计算能力。
所以,SRS4.0引入WebRTC能力的主要目的是:
1)支持浏览器无插件的从SRS服务器拉流并直接播放。
2)降低音视频数据的总延时(最低毫秒级延时)。
3)支持双向音视频能力,支持直播连麦场景。
WebRTC包括的知识点非常多,从SDP报文的生成与交换、ICE方式建立连接,DTLS握手/SRTP加解密、RTP/RTCP数据封装与传输,到面对网络抖动、带宽不足时各种提升音视频用户体验的Qos处理,每个知识点涉及的内容都非常多,本章将从WebRTC推拉流连接建立开始,通过分析音视频数据在关键类和关键函数之间的总体流向,先从整体上了解SRS4.0 WebRTC服务器模块的代码逻辑。
SRS WebRTC服务模块的初始化和启动接口在文件srs_app_rtc_server.cpp中,整体处理逻辑包括:
1)生成用于DTLS的自签名证书
2)启动UDP端口(8000)监听,处理STUN/DTLS/RTP报文
3)注册推拉流API接口
srs_error_t RtcServerAdapter::initialize()
{
......
// 此函数内部调用openssl库,生成自签名证书,用于后续的DTLS认证
if ((err = _srs_rtc_dtls_certificate->initialize()) != srs_success) {
return srs_error_wrap(err, "rtc dtls certificate initialize");
}
// 此函数内部订阅5秒定时器的超时消息,通过此消息完成一些周期性工作
if ((err = rtc->initialize()) != srs_success) {
return srs_error_wrap(err, "rtc server initialize");
}
return err;
}
srs_error_t RtcServerAdapter::run()
{
......
// 创建UDP端口监听对象SrsUdpMuxListener,默认监听8000端口
if ((err = rtc->listen_udp()) != srs_success) {
return srs_error_wrap(err, "listen udp");
}
// 向全局SrsHttpServeMux对象注册RTC模块的推拉流API
if ((err = rtc->listen_api()) != srs_success) {
return srs_error_wrap(err, "listen api");
}
// 启动_srs_rtc_manager内部协程,用于清理内部僵尸连接,回收资源
if ((err = _srs_rtc_manager->start()) != srs_success) {
return srs_error_wrap(err, "start manager");
}
return err;
}
前面我们知道,RTMP客户端和服务器之间,总是先建立一条socket连接,客户端通过此连接向服务器发送推拉流请求命令
,服务器接收到请求命令后,使用同一个socket连接传输音视频数据流。所以,RTMP协议的推拉流控制命令
和音视频数据流,总是由同一个socket连接传输,并通过不同的RTMP报文类型,实现socket复用。
WebRTC协议基于P2P/ICE技术在两个WebRTC终端之间建立UDP数据通道,当终端1向终端2发送建立连接的请求
时,一般总是要经过一个单独的信令服务器完成这些控制命令的转发(具体的做法就是两个WebRTC终端分别和信令服务器保持长连接,并用不同的客户端ID标识不同的客户端长连接,当终端1向终端2发送请求命令
时,只要在命令报文中带上终端2的客户端ID,并把请求报文发送到信令服务器,信令服务器就能将请求报文通过正确的客户端长连接转发到终端2)。
所以,任何实用的WebRTC系统,一定会有自己的信令服务器
和控制命令
,而且这部分的实现通常都是私有的,因为WebRTC标准本身就没有定义这些必要的控制命令。
下面这个草案,参考RTMP协议的推拉流URL规范: https://github.com/rtcdn/rtcdn-draft
定义了形如 webrtc://domain/会议ID/推流客户端ID
的WebRTC推拉流URL。
以视频会议为例,不同会议之间的会议ID
必须不同,同一个会议中不同客户端的推流ID
必须不同。
同时,SRS4.0通过HTTP(S)服务对外提供了推流API接口(/rtc/v1/publish/)
和拉流API接口(/rtc/v1/play/)
,下面代码是这两个API接口的注册逻辑。
srs_error_t SrsRtcServer::listen_api()
{
......
// 获取全局API管理对象SrsHttpServeMux
SrsHttpServeMux* http_api_mux = _srs_hybrid->srs()->instance()->api_server();
// 注册WebRTC拉流API对应的处理对象SrsGoApiRtcPlay
if ((err = http_api_mux->handle("/rtc/v1/play/", new SrsGoApiRtcPlay(this))) != srs_success) {
return srs_error_wrap(err, "handle play");
}
// 注册WebRTC推流API对应的处理对象SrsGoApiRtcPublish
if ((err = http_api_mux->handle("/rtc/v1/publish/", new SrsGoApiRtcPublish(this))) != srs_success) {
return srs_error_wrap(err, "handle publish");
}
return err;
}
WebRTC客户端通过推流API接口(/rtc/v1/publish/)
向SRS服务器发送推流请求命令
,此时SRS的通过如下处理流程,最终创建一个推流端接收对象SrsRtcPublishStream。
srs_error_t SrsGoApiRtcPublish::serve_http() { // 此函数为推流API的处理入口
do_serve_http(w, r, res); // 处理远端推流请求(包含客户端SDP信息),并构造请求响应
return srs_api_response(w, r, res->dumps()); // 向客户端发送请求响应(包含本端SDP信息)
}
srs_error_t SrsGoApiRtcPublish::do_serve_http() { // 处理远端推流请求,并构造请求响应
......
server_->create_session(&ruc, local_sdp, &session); // 创建会话对象和本端SDP信息
}
srs_error_t SrsRtcServer::create_session() {
......
// 参考WebRTC推拉流URL
// 以"/会议ID/推流客户端ID"字符串为Key,为每个推流端创建一个对应的SrsRtcSource对象
_srs_rtc_sources->fetch_or_create(req, &source);
// 为每个推流端创建SrsRtcConnection类型的session对象
SrsRtcConnection* session = new SrsRtcConnection(this, cid);
do_create_session(ruc, local_sdp, session); //
}
srs_error_t SrsRtcServer::do_create_session(SrsRtcUserConfig* ruc, SrsSdp& local_sdp, SrsRtcConnection* session){
if (ruc->publish_) {
session->add_publisher(ruc, local_sdp); // 为session添加推流端处理对象
}
session->initialize(); //
_srs_rtc_manager->add_with_name(username, session);// 以本地随机字符串ufrag+远端ufrag为Key,保存session对象
}
srs_error_t SrsRtcConnection::add_publisher(SrsRtcUserConfig* ruc, SrsSdp& local_sdp){
create_publisher(req, stream_desc);
}
srs_error_t SrsRtcConnection::create_publisher(SrsRequest* req, SrsRtcSourceDescription* stream_desc)
{
// 创建推流端处理对象SrsRtcPublishStream,并启动内部的SrsRtcPLIWorker协程
SrsRtcPublishStream* publisher = new SrsRtcPublishStream();
publisher->start();
}
SRS接收到用户发送的推流API(/rtc/v1/publish/)后,通过上面的函数调用栈,最终创建了SrsRtcConnection对象、SrsRtcPublishStream对象和SrsRtcSource对象。
WebRTC客户端通过拉流API接口(/rtc/v1/play/)
向SRS服务器发送拉流请求命令
,此时SRS的通过如下处理流程,最终创建一个拉流端发送对象SrsRtcPlayStream。
srs_error_t SrsGoApiRtcPlay::serve_http() { // 此函数为拉流API的处理入口
do_serve_http(w, r, res); // 处理远端拉流请求(包含客户端SDP信息),并构造请求响应
return srs_api_response(w, r, res->dumps()); // 向客户端发送请求响应(包含本端SDP信息)
}
srs_error_t SrsGoApiRtcPlay::do_serve_http() { // 处理远端推流请求,并构造请求响应
server_->create_session(&ruc, local_sdp, &session); // 创建会话和本端SDP
}
srs_error_t SrsRtcServer::create_session() {
......
// 参考WebRTC推拉流URL
// 以"/会议ID/推流客户端ID"字符串为Key,为拉流端找到对应推流端的SrsRtcSource对象
_srs_rtc_sources->fetch_or_create(req, &source);
// 为每个拉流端创建SrsRtcConnection类型的session对象
SrsRtcConnection* session = new SrsRtcConnection(this, cid);
do_create_session(ruc, local_sdp, session); //
}
srs_error_t SrsRtcServer::do_create_session(SrsRtcUserConfig* ruc, SrsSdp& local_sdp, SrsRtcConnection* session)
{
if (ruc->publish_) {
......
} else {
session->add_player(ruc, local_sdp); // 为session添加拉流端处理对象
}
session->initialize(); //
_srs_rtc_manager->add_with_name(username, session);// 以本地随机字符串ufrag+远端ufrag为Key,保存session对象
}
srs_error_t SrsRtcConnection::add_player(SrsRtcUserConfig* ruc, SrsSdp& local_sdp){
create_player(req, play_sub_relations);// 创建拉流端处理对象SrsRtcPlayStream
}
srs_error_t SrsRtcConnection::create_player(SrsRequest* req, std::map<uint32_t, SrsRtcTrackDescription*> sub_relations){
// 创建拉流端处理对象SrsRtcPlayStream,并启动拉流端处理协程
SrsRtcPlayStream* player = new SrsRtcPlayStream();
player->start();
}
srs_error_t SrsRtcPlayStream::cycle(){ // 拉流端处理协程
source->create_consumer(consumer); // 为每个拉流端创建SrsRtcConsumer消费者对象
while (true) {
consumer->dump_packet(&pkt); // 在SrsRtcConsumer消费者队列中等待并获取报文
if (!pkt) {
consumer->wait(mw_msgs);
continue;
}
send_packet(pkt);// 将SrsRtcConsumer消费者队列中的报文发送的拉流客户端
}
}
SRS接收到用户发送的拉流API(/rtc/v1/play/)后,通过上面的函数调用栈,最终创建了SrsRtcConnection对象、SrsRtcPlayStream对象和SrsRtcConsumer对象。
上面的过程只创建了针对WebRTC服务的关键对象,接下来需要分析,推拉流客户端与WebRTC服务的监听端口(8000)之间如何建立连接。WebRTC客户端与服务端之间的连接建立方式采用了类P2P私网穿透的方式。这种方式的一个最大特点就是一个WebRTC客户端向服务端发起连接请求时,事先并不知道服务端的IP地址和端口号,所以WebRTC连接建立一般包括两个阶段:
1)WebRTC客户端与服务端之间以offer和answer的方式交换包含各自IP地址+端口号信息的SDP(Session Description Protocol)报文。
2)WebRTC客户端从服务端SDP报文中获取服务端的IP地址和端口号,并以ICE(Interactive Connectivity Establishment)方式,在客户端和服务端之间建立连接,用于后续音视频数据的传输。
网上关于SDP和ICE的资料比较多,可根据需要学习、参考
https://segmentfault.com/a/1190000038272539 WebRTC SDP 详解和剖析
https://segmentfault.com/a/1190000020794391?utm_source=sf-similar-article WebRTC会话描述协议(SDP)详解
https://zhuanlan.zhihu.com/p/60684464 WebRTC 之ICE浅谈
下面是浏览器发送给SRS服务器的offer SDP,因为是trickle
模式,所以SDP中没有包含客户端的IP地址,当然这并不影响最终的连接建立。
v=0
o=- 6308787264381624235 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
a=ice-options:trickle
a=sendonly
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123
a=ice-options:trickle
a=sendonly
SRS服务端响应的answer SDP,其中candidate
属性包含了SRS服务器的IP地址和端口描述信息(192.168.9.102 8000),并且服务端采用ice-lite
模式简化了ICE协商过程。
v=0
o=SRS/4.0.140(Leo) 32138128 2 IN IP4 0.0.0.0
s=SRSPublishSession
t=0 0
a=ice-lite
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=recvonly
a=candidate:0 1 udp 2130706431 192.168.9.102 8000 typ host generation 0
m=video 9 UDP/TLS/RTP/SAVPF 125 124
a=recvonly
a=candidate:0 1 udp 2130706431 192.168.9.102 8000 typ host generation 0
接下来,浏览器向SRS服务器的8000端口发送一个Binding Request报文,服务器给浏览器回一个Binding Success Response响应。最终,推拉流客户端与SRS服务器(8000端口)建立连接。
后续,客户端和服务器之间将在此连接上完成DTLS校验,并进行音视频RTP报文的传输。
SRS4.0 WebRTC模块整体架构和处理流程是:
1)监听UDP端口(默认8000),并注册推流API接口(/rtc/v1/publish/)
和拉流API接口(/rtc/v1/play/)
。
2)推流端处理逻辑创建SrsRtcConnection对象、SrsRtcPublishStream对象和SrsRtcSource对象;
拉流端处理逻辑创建SrsRtcConnection对象、SrsRtcPlayStream对象和SrsRtcConsumer对象。
3)推拉流客户端与SRS服务器之间通过SDP交换,采用ICE方式建立UDP连接,完成DTLS安全协商。
4)最终,音视频数据从推流客户端到拉流客户端的数据流向如下图所示:
a、推流客户端
–>服务器8000端口–>SrsRtcConnection–>SrsRtcPublishStream–>SrsRtcSource–>SrsRtcConsumer数据队列
b、SrsRtcConsumer数据队列—>SrsRtcPlayStream::cycle()协程获取数据—>拉流客户端
10、SRS4.0源代码分析之WebRTC推流端处理