9、SRS4.0源代码分析之WebRTC服务总体介绍

前言:

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服务器模块的代码逻辑。


内容:

1、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;
}

2、处理推流API

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对象。

3、处理拉流API

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对象。

4、SDP交换与ICE建立连接

上面的过程只创建了针对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推流端处理

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值