TRTC Web SDK新架构设计解析(GMTC逐字稿)

2022-07-28 15:55:32 浏览数 (1)

腾讯实时音视频(Tencent Real-Time Communication,TRTC)是腾讯云基于 QQ 在音视频通话技术上的积累,它还结合了腾讯浏览服务 TBS WebRTC 能力与腾讯实时音视频 SDK ,为客户提供多平台互通、高品质、可定制化的实时音视频互通服务解决方案。目前腾讯云团队正在开发的 TRTC Web SDK 新架构。

在 2021 年 GMTC 全球大前端技术大会【深圳站】中,来自腾讯云的高级工程师李宇翔,重点分享了新架构中运用了哪些设计思想和技巧,来提高程序的性能和代码的可维护性。

背景介绍

腾讯云的 TRTC 产品主要提供了音视频领域的一些基础功能,并通过 SDK 供用户使用,用户可以使用 TRTC 提供的底层能力构建自己的产品。TRTC 提供的能力主要分为终端和云端两个部分:云端部分均基于腾讯云,而终端部分覆盖了主流的 Windows、macOS、iOS、Android等原生端,以及小程序等 Web 端。

WebRTC 是 TRTC SDK 使用的开源解决方案。WebRTC 本质上是前端可以调用的中间算法与模块,对前端工程师而言,相当于一个封装到浏览器内部的黑盒。WebRTC方案有自己的优势和劣势,其中优势包括无需插件即可运行、前端开发难度低、开源免费、自带 P2P 功能等。

而 WebRTC 方案的劣势也比较明显:

  1. 编解码器是封装好的,无法自定义;
  2. 传输方式固定,无法自定义;
  3. 服务端需要适配 WebRTC ,即便云端有很多自制功能,接入了自有体系,依旧需要通过WebRTC 实现沟通;
  4. WebRTC 无法运行在 Worker 中,只能运行在主线程上。

为了解决上述问题,腾讯云团队自行开发了一套 WebRTC 的替代方案:

该替代方案主要分为算法层和传输层两部分。传输层的选择更加灵活,支持现有和未来出现的各种 Web 传输方案,如 DataChannel、WebSocket、WebTransport等;算法层面支持WebRTC 的所有模块,通过 Webassembly 的方式在浏览器运行,从而支持自定义编解码。该方案要求 M94 版本以上的浏览器环境,考虑到目前 M94 刚刚发布不久,就已经有40%的占有率,这个方案的兼容性是比较乐观的。

新老架构对比

从下图中可以明显发现,新旧方案 SDK 使用的主要技术是有一些差异的:

从架构层面来看,WebRTC SDK的架构如下图所示:

可以看到Client、LocalStream、RemoteStream 是对外暴露的部分,为了实现平稳迭代升级,如下图所示,新方案的第一个版本整体架构没有变化。

在改造过程中,团队还对原有的开源代码做了优化。以 Client 类为例,原始代码多达3500行,现在经过分层优化实现了大幅瘦身;

老方案的代码以 JavaScript 为主,很容易出错,所以新方案转向了 TypeScript 。TypeScript的强类型检查、引用跳转、类型约束等特性可以明显提升开发效率。与此同时,TypeScript 可以支持渐进式迁移,项目无需一次性全部转换到 TS ,迁移成本非常低。

如何解决性能问题

向新方案第一次迁移的过程中,团队就遇到了性能问题,其原因在于主线程的压力过大。典型的前端脚本执行机制如下图所示:

一般情况下,浏览器以 60hz 的速度渲染页面,每 16ms 渲染一次 UI 并执行脚本,16ms 中剩余的时间 CPU 会空闲,但由于界面特别复杂,渲染耗时过长,脚本执行时间就会不足,导致脚本无法正常执行:

第二种情况,JS 代码中包含了 wasm 执行部分,导致脚本执行时间过长,留给渲染的时间减少,就会导致渲染卡顿的现象。

为此,新方案选择用 Worker 通过分工方式来降低 CPU 占用。主线程主要做渲染与采集,其它工作尽可能放到 Worker 中执行。Worker 中有定时器可以做到精确执行,不受 UI 线程渲染影响。加入Worker后整体架构如下图所示:

其中 Worker 线程负责与后端网络通讯,后端先同 Worker 通讯,Worker 再把一部分数据传给主线程。这里的设计原则是尽量减少线程间通讯,避免主线程与 Worker 之间通讯过于频繁增大开销。为此,Worker 端需要更为复杂的设计,包含了大部分耦合度较高的主要逻辑:

优雅管理生命周期

生命周期是指一件事情从开始到终结的完整周期,例如进房到退房、发布到取消发布、订阅到结束订阅等。其中,能够被用户感知到的周期(如进房到退房)称为宏观生命周期。

在开发环境中,一些复杂页面可能并没有明显的开始与结束的区分。如果程序开发中出现大量生命周期,其中存在众多异常情况,程序就要在每一种生命周期出现异常时做判断。如何以更好的模式,优雅地管理这些生命周期,是新 SDK 架构面临的挑战。

除宏观生命周期外还有微观生命周期。以一场分享活动举例,活动开始到结束的过程相当于程序启动到退出的过程。每一位参会者都有自己独立的生命周期,就像程序中每一个生成的对象都有自己的生命周期一样。正常情况下,分享活动会按照流程有序推进直到结束,但有时遇到天气、灾害等不可抗力的因素时,活动就需要立刻结束,这就相当于程序中的突发事件导致生命周期发生了变化。

为了更好地处理微观生命周期,团队引入了 ReactiveX 响应式编程技术。

响应式编程其实就是发布订阅者模式。上图左边的观察者与右边的订阅者形成了一个宏观生命周期。左边开始订阅,生命周期开始;左边的发布者发布结束,生命周期就完成。中途出现异常或者取消订阅,生命周期也会终结。

从上图的代码视角来看,左侧程序主要采集生命周期,用来采集音视频数据。右边代码负责编码。这两者形成了一个生命周期,先采集数据然后编码,两者互相依赖。出现异常时,例如编码生命周期突然结束,就需要通知采集周期同样结束,反之亦然。

使用 ReactiveX 可以清晰地撰写上述生命周期相关的代码,这种编程方式与常见的事件驱动编程模型是有很大不同的。在事件驱动模型中涉及大量回调,程序开发的视角类似于一场活动的主办方视角。主办方要事无巨细地关注活动中的所有细节,开发者也需要对每一个事件的所有逻辑做好处理,这样才能保证程序正常运行。

而发布订阅模式可以称为参与者视角。每一位参与者只关心最终的调遣。比如通知演讲人演讲即将开始,演讲人不用关心之前发生了哪些事件,只要在通知自己开始的时候上台演讲即可。演讲结束亦是如此。

这种参与者视角不直接处理回调,而是将原来的回调转化为一个信号,各个信号再自由组合成需要的信号。组合完成后的信号就是最后要处理逻辑的事件。

上图的 ReactiveX 三极管模型中,有一个主信号不断发出数据,还有控制信号用来终止主信号和响应逻辑。主信号、响应逻辑和控制信号等都有自己的微观生命周期,它们整体形成宏观生命周期。宏观生命周期结束时,就可以通知所有微观生命周期自动结束。宏观生命周期可以通过控制信号控制,而所有的控制信号也是信号,可以被其他控制信号所控制。各种控制信号的组合最终可以实现级联控制:

为了让整个过程更加优雅无痛,团队引入了 Go 语言中的 Context 模型,它是一个可以取消的轻量对象,可以携带少量数据、级联结束,还可以被传递和持有。

例如进房之后,首先创建 roomCtx ,推拉流都依赖于 roomCtx。推拉流操作都可能中途启动或停止,但如果 roomCtx 退房就要结束所有周期。

传统代码要在退房代码中写很多判断。比如退的时候判断是否正在推流,如果是就停止推流,等等。

改用新方式进行实现会优雅许多:在退房的回调函数里只写一行代码取消 Context 。它的取消会触发子级 Context 全部取消,自动将其他微观生命周期全部终止。这样就无需关心其它生命周期的状态,只需关心 Context 是否结束即可。这样就实现了更优雅的生命周期管理,有效减轻了开发过程中的心智负担。

目前这套 SDK 新方案还在开发之中,预计在性能、稳定性等指标上会有明显提升。

0 人点赞