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 = 收到Offerpc = 创建RTCPeerConnection对象监听pc的创建新收发器的事件await pc.SetRemoteDescription(offer)answer = await pc.CreateAnswer()await pc.SetLocalDescription(answer)等待IP地址、端口等信息获取完成answer = pc.GetLocalDescrption()通过服务器中转将answer发送给Apc.等待连接成功的事件
}
用户A {answer := // 收到answerpc.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.rhkb.cn/news/13498.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Selenium记录RPA初阶 - 基本输入元件

防止自己遗忘,故作此为记录。 爬取网页基本元件并修改后爬取。 包含元件: elements: dict[str, str] {"username": None,"password": None,"email": None,"website": None,"date": None,"ti…

Ubutun本地部署DeepSeek R1

目录 一、本地部署&终端命令行交互 二、网页端交互 三、参考链接 一、本地部署&终端命令行交互 Ollama 是一个轻量级的大语言模型管理工具,支持 Windows / Mac / Linux。 Ollama官网:Ollama # 下载安装ollama curl -fsSL https://ollama.co…

NacosRce到docker逃逸实战

NacosRce到docker逃逸实战 1、Nacos Derby Rce打入内存马 这个漏洞的原理大家应该都知道&#xff0c; 2.3.2 < Nacos < 2.4.0版本默认derby接口未授权访问&#xff0c;攻击者可利用未授权访问执行SQL语句加载构造恶意的JAR包导致出现远程代码执行漏洞。 在日常的漏洞挖…

保姆级教程Docker部署KRaft模式的Kafka官方镜像

目录 一、安装Docker及可视化工具 二、单节点部署 1、创建挂载目录 2、运行Kafka容器 3、Compose运行Kafka容器 4、查看Kafka运行状态 三、集群部署 四、部署可视化工具 1、创建挂载目录 2、运行Kafka-ui容器 3、Compose运行Kafka-ui容器 4、查看Kafka-ui运行状态 …

[创业之路-286]:《产品开发管理-方法.流程.工具 》-1- IPD两个跨职能团队的组织

IPD&#xff08;集成产品开发&#xff09;中的两个重要跨职能组织是IPMT&#xff08;集成产品管理团队&#xff09;和PDT&#xff08;产品开发团队&#xff09;。 在IPD&#xff08;集成产品开发&#xff09;体系中&#xff0c;IRB&#xff08;投资评审委员会&#xff09;、IPM…

【基于SprintBoot+Mybatis+Mysql】电脑商城项目之修改密码和个人资料

&#x1f9f8;安清h&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;【Spring篇】【计算机网络】【Mybatis篇】 &#x1f6a6;作者简介&#xff1a;一个有趣爱睡觉的intp&#xff0c;期待和更多人分享自己所学知识的真诚大学生。 目录 &#x1f383;1.修改密码 -持久…

02.06 网络编程_概述

思维导图 总图&#xff1a;

初阶数据结构:树---堆

目录 一、树的概念 二、树的构成 &#xff08;一&#xff09;、树的基本组成成分 &#xff08;二&#xff09;、树的实现方法 三、树的特殊结构------二叉树 &#xff08;一&#xff09;、二叉树的概念 &#xff08;二&#xff09;、二叉树的性质 &#xff08;三&#…

【LeetCode】day15 142.环形链表II

142. 环形链表 II - 力扣&#xff08;LeetCode&#xff09; 题目描述 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则…

2025.2.6

一、C思维导图&#xff1a; 二、C&#xff1a; 三、注释代码 1> 配置文件&#xff1a;.pro文件 QT core gui # 引入的类库&#xff0c;core表示核心库 gui图形化界面库greaterThan(QT_MAJOR_VERSION, 4): QT widgets # 超过版本4的qt&#xff0c;会自动加widgets…

CSS(三)less一篇搞定

目录 一、less 1.1什么是less 1.2Less编译 1.3变量 1.4混合 1.5嵌套 1.6运算 1.7函数 1.8作用域 1.9注释与导入 一、less 1.1什么是less 我们写了这么久的CSS,里面有很多重复代码&#xff0c;包括通配颜色值、容器大小。那我们能否通过js声明变量来解决这些问题&…

643. 子数组最大平均数 I

目录 一、题目二、思路2.1 解题思路2.2 代码尝试2.3 疑难问题 三、解法四、收获4.1 心得4.2 举一反三 一、题目 二、思路 2.1 解题思路 和之前一样&#xff0c;用一个sum来存储统计情况&#xff0c;窗口滑动边统计&#xff0c;用两个for循环&#xff0c;一个初始化&#xff0…

go数据结构学习笔记

本博文较为完整的实现了go的链表、栈&#xff0c;队列&#xff0c;树&#xff0c;排序&#xff0c;链表包括顺序链表&#xff0c;双向链表&#xff0c;循环链表&#xff0c;队列是循环队列&#xff0c;排序包含冒牌、选择 1.链表 1.1 顺序链表 type LNode struct {data intn…

机器学习--python基础库之Matplotlib (1) 超级详细!!!

机器学习--python基础库Matplotlib 机器学习--python基础库Matplotlib0 介绍1 实现基础绘图-某城市温度变化图1.1绘制基本图像1.2实现一些其他功能 2 再一个坐标系中绘制多个图像3 多个坐标系显示-plt.subplots(面向对象的画图方法)4 折线图的应用场景 机器学习–python基础库M…

Java 23新特性

文章目录 Java 23新特性一、引言二、Markdown文档注释&#xff08;JEP 467&#xff09;示例 三、ZGC&#xff1a;默认的分代模式&#xff08;JEP 474&#xff09;1. 为什么要引入分代模式2. 使用分代模式的优势3. 如何启用分代模式 四、隐式声明的类和实例主方法&#xff08;JE…

【redis】数据类型之string

字符串类型是Redis最基础的数据结构。首先key都是字符串类型&#xff0c;而且其他几种数据结构都是在字符串类型基础上构建的&#xff0c;所以字符串类型能为其他四种数据结构的学习打下基础。 字符串类型的值实际可以是字符串&#xff08;简单的字符串、复杂的字符串&#xf…

前部分知识复习05

一、多级渐远贴图MipMap 选择贴图&#xff0c;可以勾选贴图的多级渐远效果 [IntRange]_MipMap("MipMap",Range(0,12))0 //多级渐远贴图的LOD调节滑杆 _MipMapTexture("MipMapTexture",2D)"white"{} //定义多级渐远贴图 多级渐远贴图的采样…

[高等数学]曲率

一、知识点 &#xff08;一&#xff09;弧微分 设函数 f ( x ) f(x) f(x) 在区间 ( a , b ) (a,b) (a,b) 内具有连续导数。 在曲线 y f ( x ) yf(x) yf(x) 上取固定点 M 0 ( x 0 , y 0 ) M_0(x_0,y_0) M0​(x0​,y0​) 作为度量弧长的基点&#xff0c;并规定依 x x x 增…

openGauss 3.0 数据库在线实训课程2:学习客户端工具gsql的使用

openGauss数据库状态查看 前提 我正在参加21天养成好习惯| 第二届openGauss每日一练活动 课程详见&#xff1a;openGauss 3.0.0数据库在线实训课程 学习目标 学习openGauss数据库客户端工具gsql的使用。 课程作业 gsql是openGauss提供在命令行下运行的数据库连接工具&am…