当前位置: 首页 > article >正文

B站自研的第二代视频连麦系统(上)

导读 

本系列文章将从客户端、服务器以及音视频编码优化三个层面,介绍如何基于WebRTC构建视频连麦系统。希望通过这一系列的讲解,帮助开发者更全面地了解 WebRTC 的核心技术与实践应用。

背景

在文章《B站在实时音视频技术领域的探索与实践》中,提到了直播行业从传统娱乐直播发展到教育、电商等新形式,用户对实时互动直播的需求增加。B站基于WebRTC的开发了一套视频连麦系统:这套系统优先选择UDP协议以保证低延迟,必要时降级为TCP;且使用前向纠错和后向纠错结合解决丢包问题;并根据网络状况动态调整音视频码率和发送速率,确保实时性和画质。

但是这套视频连麦系统是提取了WebRTC的部分模块组合而成的,对于上游代码仓库的后续升级有较高维护成本,且与使用高层级抽象接口的Web浏览器端无法很好兼容互通。所以在使用了一段时间后,我们决定对其进行重构,改为使用WebRTC的标准应用编程接口(API)进行开发。

本文为上篇,将会着重介绍终端上如何使用WebRTC的标准应用编程接口来接入视频连麦业务。

信令和直接连接

WebRTC的握手主要通过“信令交换”来完成。“信令”是一个相对抽象的术语,在实际操作中,可以用一个简化的例子来解释。我现在有两个主播需要进行视频连麦,一方主播已经准备好了摄像头画面、压缩摄像头画面的编码器、麦克风音频、压缩麦克风音频的编码器,以及用于数据传输的协议和网络地址。进行视频连麦的另一方主播,需要相应地准备好可以接收数据的网络地址、可以解析传输协议的解析器、以及用于解码这些音视频数据的解码器。

因此,发送端的主播需要告诉接收端的主播自己即将开启的视频和音频分别使用了哪种编码格式,并通过哪个IP地址和端口进行数据发送。同时,接收端的主播也需要告知发送端的主播自己可以接收音频和视频,并通过什么IP地址和端口接收数据。双方在交换了这些信息后,发送端的主播就可以将数据发送到接收端的主播的IP地址。通过这一过程,双方可以互相接收对方的声音和画面,从而实现视频连麦。

上述流程虽然理想,但实际操作中可能面临一些挑战。例如,接收端的主播无法解析发送端的数据或解码其音视频,这种情况该如何处理?为了尽量减少这种问题的发生,实际使用中,发送端通常会一次性列出多种不同格式的编码。接收端则从中选择其能识别的格式并通知发送端。发送端随后仅使用双方兼容的编码格式进行传输。同样,对于传输协议,假如发送端能传输前向纠错的数据包以改善高延迟网络下的通信质量,但接收端无法识别这些数据包,那么传输这些数据包反而会占用网络资源。

在WebRTC中,“信令”是一种用于记录和传输会话描述协议(Session Description Protocol, SDP)的机制。SDP最终表现为一个包含编码格式、传输协议、IP地址、端口及一些附加信息的长字符串。

当需要建立WebRTC通信连接时,两个用户会互相传递这样一个字符串。一个用户将该字符串发送给另一个用户,接收方随后也会返回一个类似的字符串。通过这个过程,双方就能互相了解使用什么格式和协议,通过哪个IP地址和端口进行数据传输,从而实现通信连接。

在此过程中,想要变更传输细节的一方会发送一个称为Offer的SDP字符串,另一方在解析Offer后修改本地状态,随后生成Answer并传回。这种来回交换信令的过程被称为协商(negotiate)。理解这一过程时,可以将其类比为一次双方状态同步的远程过程调用,这或许会更容易理解。

以实际例子为例,用户A的SDP字符串中详细列出了以下信息:音视频的收发地址为10.0.0.2,端口为17723;传输协议使用SRTP,视频的编码和解码均使用H.264,SSRC为114514(由于音视频共用一个端口进行收发,因此需要一个“编号”来区分发出的数据包是音频还是视频,这个编号就是SSRC),音频的编码和解码均使用OPUS。用户B收到该SDP字符串后,会解析其中的内容,从而知道往10.0.0.2的17723端口发送何种数据,确保用户A能正常处理。然后,用户B也会回传一个类似的SDP字符串,包含上述信息。用户A同样会解析该字符串,确保后续发送的音视频数据能够被用户B接收并正常处理。

以伪代码表示,由一台服务器在两个用户之间中转数据,流程大致如下:

用户A {
    pc = 创建RTCPeerConnection对象
    给pc添加视频收发器(Transceiver)用于发送或接收
    给pc添加音频收发器(Transceiver)用于发送或接收
    offer = await pc.CreateOffer() // offer里包含了IP地址、端口和收发器能使用的协议、编码等信息
    await pc.SetLocalDescription(offer)
    等待IP地址、端口等信息(即:Candidate)获取完成
    offer = pc.GetLocalDescription()
    通过服务器中转将offer发送给B
}
用户B {
    offer = 收到Offer
    pc = 创建RTCPeerConnection对象
    监听pc的创建新收发器的事件
    await pc.SetRemoteDescription(offer)
    answer = await pc.CreateAnswer()
    await pc.SetLocalDescription(answer)
    等待IP地址、端口等信息获取完成
    answer = pc.GetLocalDescrption()
    通过服务器中转将answer发送给A
    pc.等待连接成功的事件
}
用户A {
    answer := // 收到answer
    pc.SetRemoteDescription(answer)
    pc.等待连接成功的事件
}

在收到连接成功的事件之后,就可以通过收发器的接口和回调发送和接收音视频数据了。

因为WebRTC是一种比较成熟的技术,相关的示例资料在网上也好找,能解释这个字符串里哪些代表什么意思,但篇幅特别长,这里就不赘述了。

选择性转发服务器

在业务玩法逐渐变得复杂之后,这种用户之间的连接形式就应付不过来了。经常看直播的小伙伴都知道,网络直播的视频连麦会出现人传人的现象:一开始是两个人,然后变成三个,四个...九个,越来越多。如果连麦是用户之间直接连的,假设主播甲乙丙丁在视频连麦,主播甲就要把自己的音视频数据发给乙丙丁发三遍啊三遍,而且乙丙丁也逃不掉也得这么发。现在中国的家用宽带大部分是上传远小于下载的,结果就是人一多就可能又卡又糊了。

鉴于是同样的数据发这么多遍,如果有一台服务器能帮我把这个数据发给需要接收的人,那么我自己就只要发一遍给服务器就够了。所以B站就设计了这样的服务器来帮用户转发数据,这样主播就只要发一份给服务器,服务器发给另外三个人,这样正好适配了前面说中国的家用宽带大部分是下载远大于上传的特点。

服务器也运行一套WebRTC的模块,这样客户端连人和连服务器就没什么区别,也是通过交换SDP。所以服务器照常收offer、给客户端回answer,客户端就能和服务器连上,不需要区分对面是普通人还是服务器。服务器用这种方式和所有在同一个“房间”里连麦的人建立了连接;这个“房间”内的用户只和服务器连接,服务器在这些人之间有选择性地转发数据(例如,用户A只请求B和C的数据,那么A的数据不会被发回来,D的数据也不会发回给A),通过这种方式就可以实现多人连麦了。

关于选择性转发服务器的细节,将会在单独的一篇详细剖析。

信令状态

在由用户之间直接连接变成只与服务器连接之后,会出现单个RTCPeerConnection实例中,使用多个媒体收发器来接收来自不同视频连麦对手的数据的需求。考虑到不同的视频连麦对手使用的编码器可能有不同(举个例子,电脑性能好的用户可以使用AV1编码来发送视频数据,而电脑性能一般的用户只能使用H.264来发送视频数据),并且在一个视频连麦的“房间”内,参与的主播又是可以随时进出房间,所以不同媒体收发器需要协商不同的编解码设置,且媒体收发器要动态增加和删除。在上一节的伪代码中演示了如何在两个用户间创建连接,伪代码中完成了所有媒体收发器的创建然后才开始使用SDP进行协商,并没有涉及连接建立之后再添加或者删除收发器的操作。

这边我们引入一个新的概念:信令状态。在上面的例子中,对LocalDescription、RemoteDescription进行Set操作之后,信令状态就会改变。信令状态只能遵循一定的顺序变化。一个最简单的典型流程是:

图片

在这个信令交换的流程里面,需要重点观察stable, have-local-offer, have-remote-offer三个状态,这个状态的变化,通过RTCPeerConnection上的signalingstatechange事件可以监听变化;通过signalingState属性可以获取状态;遵循以上流程的话,webrtc就不会老报错。同时negotiationneeded事件指明了是不是需要进行信令交换,需要的时候事件会触发……这么说感觉很难懂,套个例子好理解点。

如果将状态机和事件引入上述用户直接建连的例子中。用户A创建了RTCPeerConnection对象,然后在对象上添加音频和视频收发器。注意,此时negotiationneeded事件会触发,意味着如果想要连接对手知道你创建了收发器,需要和它进行一次SDP交换。于是,A这里调用createOffer方法,生成己方的Offer SDP,并使用setLocalDescription更新本地的会话描述;此时,RTCPeerConnection的信令状态会变为have-local-offer。然后,A的Offer SDP通过网络传输到连接对手B那边,B也创建RTCPeerConnection,然后将A的Offer通过setRemoteDescription设置进去,此时B的RTCPeerConnection信令状态会变为have-remote-offer。B调用createAnswer生成己方的Answer SDP,并使用setLocalDescription更新本地的会话描述,此时B的信令状态变成stable。B的Answer SDP发送到A那边,A使用setRemoteDescription更新远程描述(即,连接对手的描述),A的状态也变为stable。这样一次添加收发器的流程就完成了,并且两方收发器的状态同步。

于是我们现在了解了引入negotiationneeded事件和signalingState属性之后,动态修改媒体收发器的事情就变得简单了。在连接已经建立之后,如果一方添加、删除或者修改媒体收发器,negotiationneeded事件会再次触发,此时再进行一次上述SDP交换流程,连麦双方的状态就能重新同步。

数据通道

从上面的流程中可以看出,WebRTC和网络直播中常用的RTMP、HTTP协议有很大的不同。使用RTMP推流的时候,是建立一个TCP连接,完成RTMP协议握手,然后指定数据传输的“媒体流名称”等信息,最后实时发送音视频数据流;而通过HTTP传输直播流的方式,是建立一个TCP连接,然后通过HTTP动词“GET”指定需要拉取的媒体流名称,然后服务器返回一个HTTP状态码,并持续发送音视频数据流。如果类比上述两种方式,那么WebRTC对于开发人员来说将是这样的:双方通过IP地址和端口等信息建立WebRTC连接,A添加媒体收发器后B马上收到事件回调,B这边回调函数执行完之后A收到操作完成的信息。但实际上A和B建立连接之后,只能收发媒体流;这些媒体收发器的控制信息,通过额外的SDP交换来完成,只要双方没有经过这种手动交换SDP的过程,那么一方修改了媒体收发器的状态,WebRTC内部不会给你进行远程过程调用(Remote Procedure Call)啥的,你不手动做SDP交换另一方就不会知道。

为了方便进行这种SDP交换流程,WebRTC在媒体收发器之外还提供了“数据收发器”——数据通道(Data channel)。数据通道可以传输非音视频音视频数据,内部不会像媒体收发器那样进行音视频的编码和解码,而是原原本本把调用发送函数时候传入的数据发给另一端。这样,在第一次进行SDP交换建立连接的时候,可以只创建一个数据通道完成建连,后续再添加、删除、修改媒体收发器的时候,就通过数据通道来传输SDP字符串,不再需要准备额外的渠道来收发SDP完成协商。

业务动作

对于实际在线上使用的视频连麦来说,需要一些远程过程调用来完成业务动作,这些远程过程调用的请求也会使用数据通道进行传输。以最基础的必要动作为例,视频连麦是需要区分连麦房间的,主播ABCD在进行连麦的同时,主播EFGH也可以进行视频连麦,而且ABCD和EFGH不会互相看见对方。所以需要有一个远程过程调用来告诉服务器,当前的视频连麦会话是属于哪一个房间的。在音视频传输方面,也分为“我要发送音视频”和“我要接收某某人的音视频”这样的操作。所以需要自己设计一套协议,表明这个数据是请求还是响应或者是事件通知之类,具体是哪个远程过程调用方法,携带什么参数。然后把数据结构以protobuf、messagepack、json等形式序列化之后通过数据通道发送。

总结

下面开始视频连麦的技术总结。

B站自研的第二代视频连麦系统使用标准WebRTC接口,初始状态下使用专门的接口获取服务器的信息,基于服务器信息创建只有一个数据通道的SDP完成信令协商,与服务器建立连接;

视频连麦过程中只与服务器建立连接、不与连麦对手直接建立连接,通过服务器在不同参与者之间转发音视频数据;

通过数据通道来回传输远程过程调用的请求和响应,包括加入房间、发布和接收音视频流的请求、执行房间管理操作等;涉及到音视频变更的,请求和响应需要携带SDP字符串。

通过这种方式,视频连麦能力可以使用同样的逻辑流程运行于web端、android端、iOS端、Windows端,不会像第一代那样受限于web端无法调用内部模块而无法在网页上运行。

预告

基于webrtc在客户端完成了包括连接建立和视频连麦业务需要的音视频发布、订阅等操作后,后续将介绍选择性转发服务器如何接受这种形式的连接,响应发布订阅请求,并完成包括数据转发、录像留存、业务方远程过程调用接口等后端功能。

-End-

作者丨雷鸣、大熊哥


http://www.kler.cn/a/534947.html

相关文章:

  • Mac 终端命令大全
  • LPJ-GUESS模型入门(一)
  • 设计模式——策略模式
  • Windows电脑本地部署运行DeepSeek R1大模型(基于Ollama和Chatbox)
  • 通过C/C++编程语言实现“数据结构”课程中的链表
  • [数据结构] 线性表和顺序表
  • 拧紧“安全阀”,AORO-P300 Ultra防爆平板畅通新型工业化通信“大动脉”
  • .net的一些知识点3
  • Windows本地部署DeepSeek-R1大模型并使用web界面远程交互
  • 网络面试题(第一部分)
  • 7.攻防世界 wzsc_文件上传
  • 深度学习与搜索引擎优化的结合:DeepSeek的创新与探索
  • Excel中对单列数据进行去重筛选
  • npx tailwindcss init报错npm error could not determine executable to run
  • Langchain教程-1.初试langchain
  • Spring 核心技术解析【纯干货版】- X:Spring 数据访问模块 Spring-Orm 模块精讲
  • Golang: 对float64 类型的变量进行原子加法操作
  • ESP32开发学习记录---》GPIO
  • 第四十六天|动态规划|子序列|647. 回文子串,5.最长回文子串, 516.最长回文子序列,动态规划总结篇
  • Mac 终端命令大全
  • 记录 | WPF创建和基本的页面布局
  • S4 HANA (递延所得税传输)Deferred Tax Transfer - S_AC0_52000644
  • 基于Hexo实现一个静态的博客网站
  • 本地机器上便捷部署和运行大型语言模型(LLM)而设计的开源框架Ollama
  • 《利用原始数据进行深度神经网络闭环 用于光学驻留空间物体检测》论文精读
  • Temperature、Top-P、Top-K、Frequency Penalty详解