webrtc原理及项目经验(SRS4.0源代码分析之WebRTC服务总体介绍)

1、前言:

WebRTC是一个开放的Web标准,用于支持在浏览器之间的语音、视频和通用数据的双向实时通信。在以Google为首的大厂推动下,WebRTC各项技术逐渐成熟并标准化,成为各种主流浏览器都支持的基于Web的实时音视频通信解决方案。 WebRTC本身是一个应用在客户端的类P2P技术,SRS4.0引入WebRTC处理能力,主要是为了构建服务器的SFU能力(什么是SFU读者可自行搜索)。这里借用一个网图来说明SFU的工作原理:

webrtc原理及项目经验(SRS4.0源代码分析之WebRTC服务总体介绍)(1)

如上图所示,对于视频会议场景,一般都有多个WebRTC客户端。此时通过部署SFU服务器,每个WebRTC客户端都和SFU服务器之间建立一条针对本地音视频数据的推流连接,同时,WebRTC客户端和SFU服务器之间还可以按需建立多个拉流连接。这样做的好处是即利用了SFU服务器强大的客户端接入能力,又不会在服务端因为音视频混流消耗过多的CPU计算能力。

所以,SRS4.0引入WebRTC能力的主要目的是: 1)支持浏览器无插件的从SRS服务器拉流并直接播放。 2)降低音视频数据的总延时(最低毫秒级延时)。 3)支持双向音视频能力,支持直播连麦场景。

2、目标:

webrtc原理及项目经验(SRS4.0源代码分析之WebRTC服务总体介绍)(2)

WebRTC包括的知识点非常多,从SDP报文的生成与交换、ICE方式建立连接,DTLS握手/SRTP加解密、RTP/RTCP数据封装与传输,到面对网络抖动、带宽不足时各种提升音视频用户体验的Qos处理,每个知识点涉及的内容都非常多,本章将从WebRTC推拉流连接建立开始,通过分析音视频数据在关键类和关键函数之间的总体流向,先从整体上了解SRS4.0 WebRTC服务器模块的代码逻辑。

3、内容:3.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; }

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

C 音视频学习资料免费获取方法:关注音视频开发T哥,点击「链接」即可免费获取2023年最新C 音视频开发进阶独家免费学习大礼包!

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

3.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的资料比较多,可根据需要学习、参考

浜戣绠� - WebRTC SDP 璇﹁В鍜屽墫鏋� - 闃块噷浜戣棰戜簯 - SegmentFault 鎬濆惁 WebRTC SDP 详解和剖析

html5 - WebRTC浼氳瘽鎻忚堪鍗忚锛圫DP锛夎瑙� - 涓汉鏂囩珷 - SegmentFault 鎬濆惁 WebRTC会话描述协议(SDP)详解

WebRTC 之ICE浅谈 | 内有干货免费下载 - 知乎 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端口)建立连接。

webrtc原理及项目经验(SRS4.0源代码分析之WebRTC服务总体介绍)(3)

后续,客户端和服务器之间将在此连接上完成DTLS校验,并进行音视频RTP报文的传输。

4、总结:

SRS4.0 WebRTC模块整体架构和处理流程是:

1)监听UDP端口(默认8000),并注册推流API接口(/rtc/v1/publish/)和拉流API接口(/rtc/v1/play/)。

2)推流端处理逻辑创建SrsRtcConnection对象、SrsRtcPublishStream对象和SrsRtcSource对象;    拉流端处理逻辑创建SrsRtcConnection对象、SrsRtcPlayStream对象和SrsRtcConsumer对象。

webrtc原理及项目经验(SRS4.0源代码分析之WebRTC服务总体介绍)(4)

3)推拉流客户端与SRS服务器之间通过SDP交换,采用ICE方式建立UDP连接,完成DTLS安全协商。

4)最终,音视频数据从推流客户端到拉流客户端的数据流向如下图所示:   a、推流客户端–>服务器8000端口–>SrsRtcConnection–>SrsRtcPublishStream–>SrsRtcSource–>SrsRtcConsumer数据队列   b、SrsRtcConsumer数据队列—>SrsRtcPlayStream::cycle()协程获取数据—>拉流客户端

原文链接:9、SRS4.0源代码分析之WebRTC服务总体介绍_srs webrtc_黑板报的博客-CSDN博客

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页