一方面,疫情当下,直播成了各大业务急需的必备能力;另一方面,在公司全面推进技术中台化的大背景下,NOW直播团队将多年在直播领域的技术积累,打包输出,全面中台化转型,旨在为各业务方提供一站式直播服务,最终构建公司内完整、绿色、安全的直播生态体系。 直播中台致力于为有直播诉求的业务,提供完整的直播底层技术能力,以及配套的内容管理分发、运营管理、秩序及结算等服务。通过直播中台的建设,提升跨业务之间的技术复用以及内容共享。

●易用性:业务方只需要做最基本的配置和联调即可快速完成接入;●完整性:提供标准实现、默认UI以及完善的工具支撑,降低使用成本。接入即可运行;●扩展性:提供灵活的个性化功能点扩展能力,支持业务增减功能;直播中台不仅致力于为各业务快速提供完善的直播能力,还致力于提供更优质的领先于行业的直播各类性能指标,能够在音视频清晰度、美颜效果、视频延时、内存占用、音视频首帧速度等都处于行业领先标准,并且能稳定为业务输出来自不同业务的海量高清直播源。直播中台音视频底层紧随视频中台的最新音视频技术,并且根据业务场景调优, 在传统的视频清晰度、美颜效果、视频实时性等指标有着非常不错的表现。例如视频清晰度的对比,无论是基于已经在使用的opensdk还是正在升级的trtc,与业界竞品相比都有明显的优势。以下为腾讯直播中台清晰度与快手的对比数据:' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
除了传统的一些视频指标,sdk也提供了更极致的用户体验。
例如为了最大限度加速用户进房与切房看到视频的速度,中台实现了0首帧技术:即用户进房切房几乎看不到任何视频loading状态。相比业界普遍存在的直播秒开(即1秒内播放视频)技术,无论是从数据表现还是直观的视觉效果都要更加领先。
以下为切房场景腾讯直播中台0首帧与抖音直播的切换房间显示效果对比(秒开还是可以明显看到loading态的过渡封面闪屏):
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
直播中台0首帧
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
抖音秒开效果
中台打通了各个业务的片源,可以提供来自不同业务方的海量的高清直播源,后台音视频带宽峰值承载了3T+的量。
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
到2021年1月为止,中台为10家业务提供能力或者服务支持,包括:微视、腾讯视频、看点直播、手Q、NOW直播、腾讯体育、腾讯动漫、腾讯新闻、猜歌星球、腾讯朋友;另有理财通等业务接入中。
我们先来看下直播中台SDK需要满足的特性:
● 区别于传统SDK的大型交互组合式开放型SDK
传统SDK定义为软件开发工具包,对外提供的基本都是一套API接口,SDK相当于一个虚拟的程序包,在这个程序包中有一份做好的软件功能,这份程序包几乎是全封闭的,只有一层薄的接口可以联通外界,而且是偏向于一个专项功能。例如播放器,对外提供的就是一层播放交互接口。
这种形式适用直播中台SDK吗?答案是否定的:
1.直播中台SDK是一个具备全产品能力的SDK:包含一整套产品闭环的框架,默认UI,功能及交互逻辑,数据服务,基础能力;
2.SDK能力足够健全但又需要足够分散:比如业务方只想接入直播中台的部分功能模块,如单独接入电商模块,单独接入礼物模块等,这意味着一个全量的SDK对业务方是冗余的,并且最外层接口是不可用的;
3.SDK需满足分层接入:比如业务方只想从数据层接入,不使用中台默认的UI和逻辑实现。或者只从逻辑层接入,自己实现UI或者嵌入业务原有的页面。
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
这些要求我们的SDK是个足够开放性的SDK,并且满足以下特性:
1.一键接入的能力,迅速搭建直播能力
2.化整为零的能力,满足个性化的单功能接入,部分模块接入侧,不同层次接入。不同模块可以自由组合交互,形成业务方特有需求的SDK。
● 全方位可扩展高定制型SDK
面对一个业务非常丰富,基础模块非常多的SDK,接入方接入时,与自己的业务及基础能力大概率是有一定重合性的,并且业务方会存在很多定制需求。
经过对接入方的前期调研,我们发现定制需求几乎覆盖SDK中的任何模块:包括更换UI布局和风格、更改逻辑、更换数据请求、定制基础能力、更改基础库中的第三方库等。
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
除定制中台的已有模块外,业务方还会存在各种扩展需求,中台里会提供一批直播的基础通用功能,但肯定是无法完全满足业务方的直播场景业务的,例如业务方想在直播间页面添加一些业务玩法,例如抽奖,连麦PK玩法等等。这要求中台任意一个页面是可扩展模块的,并且页面功能可以自由组装。
做个更易懂比喻,例如一个装修好的二手房:需要有一套标准工程化的一套设施能够快速完成内部的灵活组装、自由替换、维修翻新、去旧添新等。内部的每个设施之间做到相互独立,可独立使用与更换。
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
我们知道一个二手房的翻新装修是比较难的,牵一发而动全身,对原来的组成部分很容易侵入式破坏,例如原先的电视墙是针对性做的,换个电视原来的电视墙可能就不融合得废弃换新了。
● 区别于业界方案的设计挑战
针对上述复杂的SDK模式、丰富多样的业务接入形式,业务全覆盖型的定制扩展需求,这要求我们在功能完善的前提下,整个框架足够健全,足够灵活,足够开放,并且满足全方位无侵入式定制扩展修改,这对整个终端SDK框架设计是个非常大的挑战。 并且业界无可用方案,SDK一直在快速迭代、内部优化,如何支持全方位定制,不影响业务方接入效率?一些常规的SDK定制方案,都不适合直播中台:
1.Fork代码修改:接入方Fork一份SDK代码下来做定制,比如Chrome;
面临问题:SDK升级,接入方同步非常困难。
2.分支管理:SDK针对不同的接入房拉分支,有定制就在分支里修改,比如opensdk;
面临问题:1、分支管理困难,版本混乱;2、SDK升级新能力,要往每个分支同步。
3.需要定制某个能力:就在SDK加个接口,比如Log.
面临问题:1、接口膨胀厉害;2、不具备通用性,无法支持全方位定制。
常规的SDK定制方案都不适合直播中台,因此需要搭建一套无侵入式的全方位定制框架。3.1 无侵入式全方位定制框架原理
我们提出一切皆组件的设计理念,将SDK拆成一系列的组件。业务方可以在中台之外,通过中台开放的标准扩展接口,对中台现有能力进行二次开发或者新增其他功能,做到无侵入定制。只要SDK做到一切皆组件,就可以支持全方位的无侵入定制。核心原理如下: 引擎层负责组件的注册加载以及生命周期管理,因此组件需要有标准规范。
3.2 组件标准规范
组件需要做到能被定制、被单独复用,因此必须独立,不能依赖其他组件。我们将组件采用接口化开发,将接口和实现分开。并且从物理上隔离,实现接口实现级别的单独复用与去除。' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
3.2.1 组件动态化
所有组件必须通过中台统一的工厂派发到对应的Builder来构造,Builder支持外部替换成自己的Builder,从而实现业务方可动态替换的能力。
3.2.2 Adapter+微中心的组件解耦机制
针对中台需要的框架模式,我们从两方面进行思考与设计:2、每个组件都有能单独被业务方引入的诉求,组件怎样能够单独抽离可用?1、路由Router。代表框架:阿里ArouterArouter中的IProvider是比较典型的接口解耦的方式,如果中台的组件来套用的话,一般流程是:将组件的接口下沉到base通用层,下沉后,接口方法和数据对象彼此可见,在注册服务后,组件就可以使用彼此的功能了。这种形式比较方便简单的,而且更新方法和数据对象之后,可以通过报错的形式被通知,保证安全。 Redux不是严格意义上的为组件之间解耦的框架。Redux的核心思想是数据驱动,通过数据和事件将view和业务流程解耦,将不同的业务流程相互解耦。以应用的数据为应用的核心,通过事件产生数据变化,通过数据驱动view的展示。这种思想其实也是可以应用到终端的,各组件对数据中心关心的数据进行监听,通过数据驱动来解耦。可以看到这2个框架模型其实有个共性就是中心化:接口中心和数据中心。1、SDK拆分后组件有将近100个,这么多组件接口以及相应的数据结构如果下沉到base,通用层会非常膨胀,将完全不利于组件的单独抽离使用。如果全部使用通用化数据结构是可以的,如传json,map这种字典,通用化数据结构在前端是应用比较成熟的。如果放在中台呢?对业务方及共建者将是个易用性极差的存在,因为每个组件都可能要被业务方单独拿出去二次开发和替换的,字典的可维护性是相当差的。如果不用通用化数据结构呢,那么又面临前面一样的问题,数据结构需要下沉,base膨胀。还有一个问题:如果某个组件是对安全性要求较高的,它的部分功能可能是不希望随便对其他部分可见的,这个时候显然下沉不是一个好的选择。2、业务之间的接口依赖也是不适用中台的,因为每个业务模块或者业务组件都可能被单独抽离使用,如果A依赖B的接口,难道A被拿出去时还要带着B的接口吗?为此,我们提出了更适用于中台的解耦模式:胶水适配器 + 微中心。
一个组件的Interface定义了对外的能力接口。我们又给组件加了一层接口:Adapter 。它的作用是定义对外需要的能力。2个接口各司其职:一进一出。通过这种方式,可以做到模块的完全独立。' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
有了Adapter,我们可以在组件构造器中加入一层胶水,来完成对组件的适配,有了这层适配器,组件的使用和生存环境变得非常灵活,我们可以在其中加入一些复用价值低的组装逻辑,这里也是一种动态代理模式,业务方也可以灵活将代理转向自己的业务环境来适配组件。 针对使用非常频繁通用的接口(如上报,日志等),我们保留了一份微中心,只包含少量的基础组件。这个模式不是最优秀的解耦方案,但是却是非常使用中台“台情”的。1.不同业务组件完全解耦,包括对接口的依赖,组件可单独抽离使用3.3 基于全插件的页面拆分框架
一个页面由很多特性组成,比如直播间有:主播信息、成员列表、公屏、发言、礼物、分享等等组成。业务方接入的时候,希望页面的特性可增可减,比如移除中台提供的某些特性或者增加自己的业务新特性。因此我们对所有页面按照功能特性进行拆分,如下图,细化到icon级别。拆分完后,每个页面都是由一个个特性组成,拆分时不仅仅是按某一功能区分,更要对一个功能内进行更细致的拆分、更小粒度的拆分。然后我们对这些拆分后的特性进行自由组装,能够轻松组装成我们不同的直播类型页面,如下图:
为了方便对页面的微模块进行组装,我们对页面提出模版化概念。要做到页面模版化:我们将每个页面都分为3层:顶层,业务层,底层。顶层:只放一些豪华礼物动效,状态stateUI;业务层:放各个业务模块的UI;底层:音视频渲染模块。每个页面无真实布局,只留有业务的坑位,通过业务模块组装将坑位指向需要它的UI组件,布局全部下沉到UI组件中。1、每个模板有模板配置,模板配置包含层次布局和层次模块注册;2、一个注册模块的原子单位我们称之为Module,一个模板配置由不同的Module原子单位自由组装; 3、一个页面可以对应多个模板,框架会将对应层次的根布局给到注册到对应层级的模块原子单位;4、模块拿到对应ViewStub坑位后选择性交给一个UI组件去填装;我们将模板分层中的业务层布局和业务模块注册开放出去,这样业务方可灵活定制组装自己的页面,做到更纯粹的模块管理和扩展性。3.4 基于数据驱动的Feature拆分模型
进行完页面拆分,再来看看模块内如何拆分。一个模块内部是包含UI、逻辑、数据的。那么面临的问题又来了,可能接入方要重绘UI,数据使用中台的,或者只想复用UI,数据使用自己的。我们先看下原来NOW一个单模块是如何设计的:很明显可以看出来,是一个MVC结构。这个本身在NOW的设计里是没有问题的,因为NOW是个独立的产品,只需模块间比较清晰独立,内部有一定分层,有一定扩展性即可。但是放在中台问题就来了,因为我们面临的是业务复杂的定制需求,如下图,礼物面板业务方UI需要微调。因为UI、逻辑、数据之间有一定的耦合性,当我们只想更改UI时,我们整个模块面临着重写,复用性非常差。对此,我们需要的是细化职能,我们进行第2次拆分:抽象组件。首先拆分成2大类组件:UI组件、服务组件。UI组件和服务组件之间通过胶水逻辑来衔接。我们前面讲到,将业务模块分成了2大组件:UI组件和服务组件,由胶水逻辑串接了这2个组件,这里单模块内其实是个MVP的架构,组装层相当于presenter。1、胶水和业务逻辑交杂在一起,module内逻辑有变动时,module层基本不可复用2、业务逻辑和视图组件紧密耦合,基本不可复用,组件在开发中的定位模糊 接下来我们对模块内部架构做进一步优化,采用MVP-Clean架构,细化职能,抽离胶水逻辑和可复用逻辑代码,将功能逻辑抽离成一个一个小的可复用片段。1、抽离后,弱化了拼装层职能,拼装只负责代理UI的Action,衔接数据回调刷新UI 。2、功能逻辑层专职管理业务逻辑,并且内部的逻辑片段粒度非常小,可统一化输入输出接口。UseCase执行后,统一生成一个基于LiveData的State模型,监听者为执行者module。
UseCase会产生一个UI状态临时数据。
为什么不直接让UI组件绑定数据?还是前面提到的,我们UI组件的结构体是自己内部定义的,这样可以单独拿出去用,任何模块都不用依赖一个数据base。我们通过胶水层来代理转义,将数据层的结构体转换成UI层的结构体。基于数据驱动的Feature拆分模型如下:
1、拆分后,中台模块的代码可复用和替换率极大提升;2、UI组件、服务组件、基础组件、逻辑片段,都可以复用和重写。3.5 基于制约式双亲委派模型的统一化服务管理
SDK会拆分出来很多服务组件,比如登录服务、资料服务、日志服务、上报服务、推流服务、礼物服务、消息服务等等。服务太多,面临难以管理的问题:2、数据服务非常零散,放到逻辑层创建管理非常混乱,该怎样统一管理?3、数据服务需要有稳定的生命周期,例如房间销毁后,需要停止接收房间push,停止发送房间心跳等。4、服务是有不同的生命周期维度的,某些服务又需要有同样的生命周期来维持整个系统稳定。5、一组服务的生存环境是依赖一组关键流程启动的:如账号服务、通道服务等依赖登录,房间内的主播信息、成员列表、礼物等是依赖进房的。 根据服务的生命周期和作用 ,我们其实是可以对这些服务划分作用范围的,根据直播特性,可分为以下几种: 划分作用范围后,我们可以针对每个范围的服务加一层管理层,我们称之为引擎层,为什么是引擎?因为它既是一组服务的发动机,也是创建和管理服务的地方。根据服务特性,添加三个引擎:LiveEngine、UserEngine、RoomEngine。LiveEngine与整个直播场景生命周期一致,UserEngine与用户账号生命周期一致,RoomEngine与直播间生命周期一致。EngineLogic:负责引擎的环境启动,如用户引擎中负责登录创建通道,房间引擎负责房间的进房心跳环境。ServiceLoder:负责服务的生命周期和管理。' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
有了Engine后,我们可以很轻松通过Engine获取创建服务了。但是对于不同的开发者来说仍然要去关注具体Service得由哪个Engine创建,一旦使用不当就会造成以下风险,如下图2个例子:1、在房间2个不同的模块中,我们使用了不同的Engine去获取消息服务,那么这时将会产生2个实例,这样消息数据很难同步,就会造成功能异常;2、房间内的服务如果通过大于房间生命周期的Engine创建,那么在退房时由于我们只会销毁房间引擎下的服务,那么这个服务就泄漏了。 为了彻底解决Service管理,我们借鉴类加载ClassLoader双亲委派机制,开发了一套适用于中台ServiceLoader的机制,我们称之为制约式双亲委派模型。1、我们给每一个Engine加了一个作用域配置表,只有注册到作用域配置表的服务才有权限被Engine创建。有了这个配置表,所有的服务生效边界、生命周期都是可控的。2、我们将LiveEngine设置为UserEngine的父Engine,将UserEngine设置为RoomEngine的父Engine。3、框架根据业务场景只分配给各用户模块对对用Engine的ServiceLoader,Engine对业务不可见。比如在房间,拿到的是RoomEngine的ServiceLoader,当去get一个Service时,ServiceLoader首先会判断Service是否在自己作用域,如果在,直接从自身去创建和获取已有的Service,如果不在自己作用域,再委托给父亲Engine,父亲Engine会做一样的事情,这里是个递归。这也是制约式的由来。1、避免了服务的重复加载,保障Service只能生存在自己应该存在的生命周期边界里,保证了程序的稳定性。2、服务生命周期的稳定性得到保障,不会出现滥用导致的泄露。3、开发者使用简单,屏蔽细节,无需关心service的创建者是谁,无需关心service的生命周期,这些全都由框架来保障。4、对于想从服务层接入的业务方,可以轻松利用我们的这套引擎层来快速搭建开发自己的应用。3.6 演进后的SDK整体架构
我们基于一切皆组件的设计理念,将SDK拆成了一个个的组件,为无侵入式定制打下基础。同时提出一套单页面微模块的拆分框架,做到页面特性可上可下、页面有不同的特性列表、也可以让业务方根据需要增加自己的特性。利用基于数据单向驱动的微模块拆分模型,做到一个微模块的UI可定制、逻辑也可以定制以及微模块整体可以被定制。一切皆组件后,拆分出来很多服务组件,最后我们基于制约式双亲委派模型统一化服务管理,使得服务使用更加简单。演进后的直播中台架构易用、灵活、开放,具体架构如下:3.7 架构支撑音视频能力快速升级替换
音视频能力是直播中台的重要组成部分,我们要不断更新迭代优化音视频库及音视频调优,保持行业领先。
基于这套房间模版及引擎架构,完美支持了多种音视频库的灵活切换,音视频库的快速升级替换,以及包含音视频0首帧方案等多个中台性能优化需求的输出和快速实现。
基于统一引擎的播放服务管理,保证了业务层轻松实现播放服务的单实例或者多实例的切换,并且无需过多关心复杂多变的音视频状态管理,音视频流程切换等等。整个音视频管理变的清晰明了,保证了各层轻松针对业务场景做各种音视频性能优化的需求。
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
业务层与音视频层分层清晰,分层接口与解耦做好之后,无论是更新播放器还是更换,业务层不用做任何修改。不论是之前用的ThumbPlayer,还是后续要更换的trtc播放,都可做到快速切换。
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
直播中台首创一套无侵入式全方位定制化框架,可以满足接入方的任何定制需求:开播观看定制、页面定制、UI定制、逻辑/数据定制、属性定制、基础库定制,做到定制时对SDK无任何侵入修改。在SDK快速迭代、快速优化的情况下,保证了业务方的接入效率。
中台这个概念在2015年被提出,真正火起来是在2019年后,各大厂商都开始爆发式建设中台,腾讯在这2年也开始建立了很多中台。中台到底是做什么的?中台部门提炼各业务线的共性需求,最大限度地减少“重复造轮子”。
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
中台的本质是提炼共性需求复用,如果不同的业务诉求差异太大,复用度低的话,那么还去提炼和维护中台的代价就远高于复用带来的价值了。
在前面腾讯直播中台的框架介绍中,大篇幅介绍了如何尽力满足业务方各种不同的多式多样的定制需求,因为业务方多种多样的不同业务诉求很难达到“共性”2个字,而且即使是在已沉淀为“共性需求”的模块里,无论是终端还是后台也很难完全满足业务方所有诉求,业务方多多少少要增减改里面的功能来满足自己的业务特性,所以我们希望业务方能基于一层通用共性的默认实现的SDK之上灵活扩展自己的能力。我们知道,越完善的能力定制的难度越大,特别是终端,更改诉求充斥在已体系运转的UI逻辑数据中;但是更完善的能力又能快速帮助业务接入,减小搭建成本。这里确实是个矛盾,也是我们搭建中台终端框架一开始就在重点考虑的点,更是后续中台日常面对来自四面八方业务方诉求对接时的难题所在。
中台成立业务接入后,来自业务方铺天盖地的需求都会提过来,业务方更多期望将自己的需求划为共性需求,这样一是可以让中台出人来实现需求,另外一个是可以沉淀到中台的体系内。但对于中台而言,等到其他业务都有类似需求的时候再由中台来抽象提炼,这样才能最大化复用,根据不同的业务需求来提炼共性才能尽可能避免二次改造开发 。如果中台每个需求都认为是共性需求的话,中台人力也根本支撑不住。中台的建设还是需要更多接入业务方的共建。
中台有自己的版本节奏,业务方也有自己的版本节奏,业务又期望自己能快速迭代。一个需求要沉淀到中台周期是更长的,因为要做中台通用化,配置化等,中台产出后再有业务来接入定制。我们总是在沉淀中台与业务自己快速迭代快速上线寻找平衡点。中台并不适用于业务的每个阶段,在独立业务拓展期、突破期,往往受限于中台的节奏瓶颈,很可能就慢慢脱离中台了。
最后回到这篇文章的主题架构上,架构是为业务服务的,世界上没有完美的架构,只有最适合的架构。我们的“轮子”要不断更新、迭代、进化,包括中台的数据模型、接口、架构等其实都需要根据业务发展与技术的发展不断变化。中台承载了多个相似业务的发展,架构的演进频率往往会比独立的业务架构演进要快的多。