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

news2025/2/9 1:07:15

导读 

本系列文章将从客户端、服务器以及音视频编码优化三个层面,介绍如何基于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.coloradmin.cn/o/2295007.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Python实现PDF与SVG相互转换

目录 使用工具 使用Python将SVG转换为PDF 使用Python将SVG添加到现有PDF中 使用Python将PDF转换为SVG 使用Python将PDF的特定页面转换为SVG SVG(可缩放矢量图形)和PDF(便携式文档格式)是两种常见且广泛使用的文件格式。SVG是…

[渗透测试]热门搜索引擎推荐— — shodan篇

[渗透测试]热门搜索引擎推荐— — shodan篇 免责声明:本文仅用于分享渗透测试工具,大家使用时,一定需要遵守相关法律法规。 除了shodan,还有很多其他热门的,比如:fofa、奇安信的鹰图、钟馗之眼等&#xff0…

基于物联网技术的智能寻车引导系统方案:工作原理、核心功能及系统架构

本文专为IT技术员、软件开发工程师及智能停车领域专业人士打造,旨在深入剖析智能寻车引导系统的构建与优化过程。如需获取详细解决方案可前往文章最下方获取,如有项目需求及技术合作可私信作者。 智能寻车引导系统是一种集智能化、自动化于一体的停车管理…

【React】合成事件语法

React 合成事件是 React 为了处理浏览器之间的事件差异而提供的一种跨浏览器的事件系统。它封装了原生的 DOM 事件,提供了一致的事件处理机制。 合成事件与原生事件的区别: 合成事件是 React 自己实现的,封装了原生事件。合成事件依然可以通…

Redis02 - 持久化

Redis持久化 文章目录 Redis持久化一:持久化简介1:Redis为什么要进行持久化2:Redis持久化的方式 二:RDB持久化介绍1:手动触发RDB2:自动触发RDB3:redis.conf中进行RDB的配置4:RDB优缺…

初始JavaEE篇 —— Spring Web MVC入门(上)

找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程程(ಥ_ಥ)-CSDN博客 所属专栏:JavaEE 目录 RequestMappingg 注解介绍 Postman的介绍与使用 PostMapping 与 GetMapping 注解 构造并接收请求 接收简单参数 接收对象…

Leetcode—487. 最大连续1的个数 II【中等】Plus

2025每日刷题&#xff08;210&#xff09; Leetcode—487. 最大连续1的个数 II 实现代码 class Solution { public:int findMaxConsecutiveOnes(vector<int>& nums) {int zeros 0;int ans 0;for(int l 0, r 0; r < nums.size(); r) {if(nums[r] 0) {zeros;…

【MySQL】窗口函数详解(概念+练习+实战)

文章目录 前言1. SQL窗口函数 1.1 窗口函数概念1.2 窗口函数语法1.3 常见窗口函数 1.3.1 聚合窗口函数1.3.2 专用窗口函数 1.4 窗口函数性能比较 2. LeetCode 例题 2.1 LeetCode SQL 178&#xff1a;分数排名2.2 LeetCode SQL 184&#xff1a;最高工资2.3 LeetCode SQL 185&am…

前端组件标准化专家Prompt指令的最佳实践

前端组件标准化专家Prompt 提示词可作为项目自定义提示词使用&#xff0c;本次提示词偏向前端开发的使用&#xff0c;如有需要可适当修改关键词和示例 推荐使用 Cursor 中作为自定义指令使用Cline 插件中作为自定义指令使用在力所能及的范围内使用最好的模型&#xff0c;可以…

18爬虫:关于playwright相关内容的学习

1.如何在python中安装playwright 打开pycharm&#xff0c;进入终端&#xff0c;输入如下的2个命令行代码即可自动完成playwright的安装 pip install playwright ——》在python中安装playwright第三方模块 playwright install ——》安装playwright所需的工具插件和所支持的…

docker Error response from daemon: Get “https://registry-1.docker.io/v2/ 的问题处理

docker Error response from daemon: Get "https://registry-1.docker.io/v2/ 的问题处理 最近pull 数据 发现 docker 有如下错误 文章目录 docker Error response from daemon: Get "https://registry-1.docker.io/v2/ 的问题处理报错问题检查网络连接解决方案&…

【Linux系统】线程:线程的优点 / 缺点 / 超线程技术 / 异常 / 用途

1、线程的优点 创建和删除线程代价较小 创建一个新线程的代价要比创建一个新进程小得多&#xff0c;删除代价也小。这种说法主要基于以下几个方面&#xff1a; &#xff08;1&#xff09;资源共享 内存空间&#xff1a;每个进程都有自己独立的内存空间&#xff0c;包括代码段…

123,【7】 buuctf web [极客大挑战 2019]Secret File

进入靶场 太熟悉了&#xff0c;有种回家的感觉 查看源代码&#xff0c;发现一个紫色文件 点下看看 点secret 信息被隐藏了 要么源代码&#xff0c;要么抓包 源代码没有&#xff0c;抓包 自己点击时只能看到1和3处的文件&#xff0c;点击1后直接跳转3&#xff0c;根本不出…

微服务知识——微服务拆分规范

文章目录 一、微服务拆分规范1、高内聚、低耦合2、服务拆分正交性原则3、服务拆分层级最多三层4、服务粒度适中、演进式拆分5、避免环形依赖、双向依赖6、通用化接口设计&#xff0c;减少定制化设计7、接口设计需要严格保证兼容性8、将串行调用改为并行调用&#xff0c;或者异步…

双目标定与生成深度图

基于C#联合Halcon实现双目标定整体效果 一&#xff0c;标定 1&#xff0c;标定前准备工作 &#xff08;获取描述文件与获取相机参数&#xff09; 针对标准标定板可以直接调用官方提供描述文件&#xff0c;也可以自己生成描述文件后用PS文件打印 2&#xff0c;相机标定 &…

在 Navicat 17 中扩展 PostgreSQL 数据类型 | 创建自定义域

定义域 以适当的格式存储数据可以确保数据完整性&#xff0c;防止错误&#xff0c;优化性能&#xff0c;并通过实施验证规则和支持高效数据管理来维护系统间的一致性。基于这些原因&#xff0c;顶级关系数据库&#xff08;如PostgreSQL&#xff09;提供了多种数据类型。此外&a…

Linux+Docer 容器化部署之 Shell 语法入门篇 【Shell 替代】

&#x1f380;&#x1f380;Shell语法入门篇 系列篇 &#x1f380;&#x1f380; LinuxDocer 容器化部署之 Shell 语法入门篇 【准备阶段】LinuxDocer 容器化部署之 Shell 语法入门篇 【Shell变量】LinuxDocer 容器化部署之 Shell 语法入门篇 【Shell数组与函数】LinuxDocer 容…

IDEA+DeepSeek让Java开发起飞

1.获取DeepSeek秘钥 登录DeepSeek官网 : https://www.deepseek.com/ 进入API开放平台&#xff0c;第一次需要注册一个账号 进去之后需要创建一个API KEY&#xff0c;然后把APIkey记录保存下来 接着我们获取DeepSeek的API对话接口地址&#xff0c;点击左边的&#xff1a;接口…

mysql的原理及经验

1. 存储引擎 存储引擎是MySQL的核心组件之一&#xff0c;它负责数据的存储和检索。MySQL支持多种存储引擎&#xff0c;每种引擎都有其独特的特点和适用场景。 InnoDB&#xff1a;这是MySQL的默认存储引擎&#xff0c;支持事务处理&#xff08;ACID特性&#xff09;、行级锁定和…

【漫话机器学习系列】083.安斯库姆四重奏(Anscombe‘s Quartet)

安斯库姆四重奏&#xff08;Anscombes Quartet&#xff09; 1. 什么是安斯库姆四重奏&#xff1f; 安斯库姆四重奏&#xff08;Anscombes Quartet&#xff09;是一组由统计学家弗朗西斯安斯库姆&#xff08;Francis Anscombe&#xff09; 在 1973 年 提出的 四组数据集。它们…