首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >直播中台iLiveSDK终端框架演变之路

直播中台iLiveSDK终端框架演变之路

原创
作者头像
王辅佳
修改2021-01-29 14:42:24
3.5K1
修改2021-01-29 14:42:24
举报
文章被收录于专栏:开发开发

一、直播中台的诞生背景

  1. 疫情期间,直播带货火爆全网,直播能力成为各大业务急需的能力

2. 公司内业务平台发展到一定的用户量后积累了一定量的平台UGC、PGC,需要借助直播能力利用平台的生产和消费者提升变现能力。部门之前的Now直播产品具有丰富的直播运营变现的经验(19年50亿流水规模)及技术经验,能够帮助公司其他产品快速搭建直播生态链。

3. 部门内直播产品在不断衍生孵化,产品趋于多元化,产品业务形态不一样,但模块,技术很多是可以复用的,在这个背景下,我们的技术栈急需统一,避免重复造轮子。

这样的大环境下,直播中台建设迫在眉睫。

二、直播中台SDK的前期调研与分析

1、直播中台具备什么能力?终端SDK的定位是什么?

直播中台会提供一整套直播能力,包括:登录、开播、观看、房间内互动、对公管理、管理平台、商业化等。后台会提供一套完善的PASS服务。 终端SDK的定位是对直播中台PAAS服务进行易用性封装,为业务方提供端到端的直播服务。如下:

2、终端SDK需要具备什么能力?对外是怎样的形式?

SDK全称software development kit,软件开发工具包。传统SDK对外提供的基本都是一套API接口。SDK相当于一个虚拟的程序包,在这个程序包中有一份做好的软件功能,这份程序包几乎是全封闭的,只有一个小小接口可以联通外界。

这样的形式显然是不适用直播中台SDK的:

  1. 中台是一个具备全产品能力的SDK,包含一整套产品闭环的UI,逻辑,数据,基础能力;
  2. 当业务方只想接入直播中台的部分功能,比如只接入电商模块,只接入礼物模块等,这意味着一个全量的SDK对业务方是累赘的,并且最外层接口是不可用的;
  3. 一些业务方只想从数据层接入,不使用中台默认的UI和逻辑实现。或者只从逻辑层接入,自己实现UI或者嵌入业务原有的页面。

这些要求我们的SDK是个足够开放性的SDK 并且满足以下特性:

  • 一键接入的能力,迅速搭建直播能力
  • 化整为零的能力,满足个性化的单功能接入,部分模块接入侧,不同层次接入

直播SDK既然是一个业务非常丰富,基础模块非常多的SDK,那么和接入方自己的业务及基础能力大概率是有一定重合性的,并且业务方会存在很多定制需求。经过对接入方的前期调研,我们发现几乎所有模块都可能需要被定制。  例如:

  1. 业务方都有自己的UI风格,中台默认界面很多是不满足业务需求的,业务需要更改UI;
  2. 在直播场景需要展示一些业务自己的数据信息,比如人气值,走业务方自己的关注链体系等;
  3. 逻辑需要更改,比如在线人数的计算方式,业务自己有写算法逻辑,替换SDK的默认逻辑;
  4. 基础能力如网络监听,数据上报等能力业务方需要换成自己已有的;
  5. 基础库如:播放器、图片库、下载库业务需要换成自己已有的。

如下图:


三、中台SDK框架面临的困难和挑战

经过前期的调研和分析,基本确定了整个SDK的设计目标。

针对上述复杂的业务接入模式,和各种业务不同的接入定制需求,对整个终端SDK框架设计是个非常大的挑战。

这要求我们在功能完善的前提下,整个框架足够健全,足够灵活,足够开放

如同一个装修好的房子一样:既有一个完整的功能体验,里面的家具是相互搭配的,但每个家具又是相互独立的,可以灵活组装,每个家具可以单独使用,也可以针对其中的某个家具进行替换或者维修翻新,又可以引入新的家具或者移除已有的家具。

如上述的关键词,这要求我们的SDK满足以下特性:  


四、SDK框架方案探索

1、页面层级的高粒度业务模块拆分

因为模块可复用要求非常高,首先我们对所有页面按照功能特性进行拆分,如下图,细化到icon级别。

每个页面都是模块化的页面,不仅仅是按某一功能区分,更要对一个功能内进行更细致的拆分,更小粒度的拆分。

然后我们对这些拆分后的模块进行自由组装,能够轻松组装成我们不同的直播类型页面,如下图:

拆分后,我们的目的是:

  1. 不同页面按需使用模块组装
  2. 模块灵活复用,灵活插拔,业务灵活增减功能模块

2、业务模块内的精细化组件拆分

进行模块拆分后还是远远不够的,一个模块内部是包含UI,逻辑,数据的。 那么面临的问题又来了,可能接入方要重绘UI数据使用中台的,或者只想复用UI,数据使用自己的。 我们先看下原来now一个单模块是如何设计的:

很明显可以看出来,是一个MVC结构。 这个本身在Now的设计里是没有问题的,因为now是个独立的产品,只需模块间比较清晰独立,内部有一定分层,有一定扩展性即可。 但是放在中台问题就来了,因为我们面临的是业务复杂的定制需求,如下图,礼物面板业务方UI需要微调。

因为UI,逻辑,数据之间有一定的耦合性,当我们只想更改UI时,我们整个模块面临着重写,复用性非常差。 对此,我们需要的是细化职能,我们进行第2次拆分:抽象组件。

首先拆分成2大类组件:UI组件,服务组件。并且全部接口化,UI组件和服务组件之间通过胶水逻辑来衔接。

组件拆分的原则是: 1、组件mock数据后可单独运行 2、组件可独立复用和替换 拆分后组件对外只有接口,全部实行接口依赖,这样我们的UI组件,数据服务都是可以单独被复用,重写和替换的。

3、基础模块何去何从

中台内有很多基础模块和引入的第三方库,常用做法是我们一般会对基础功能封一层Util,使用到第三方的模块对第三方库进行implements。

针对以下几点进行考虑,问题又来了 1、大部分第三方库接入方是有的-> 三方库引入冲突 2、部分Util业务有,如Log ,系统api接口 -> 功能累赘 3、怎样最大化减包,怎样资源利用最大化 为此,我们决定all in component,也就是中台的所有模块都组件化,包括基础功能和第三方库,我们加入了2种组件:基础功能组件和第三方库包装组件,这2类组件都归类到服务组件。

这些基础功能也全部接口化,所有的基础功能和业务组件一样都需要可以支持到复用、重写、替换和去除。

4、组件标准化

前面提到,组件是分为接口和实现的,我们不仅要做到组件级别的复用,更要做到接口级别,实现级别的单一复用。 因为当某一个功能实现改变时,接口是可以复用的,实现如果可以去除掉,将得到最大程度的减包。 为此,我们将一个组件的接口和实现全部独立lib工程,接口与实现隔离,如下图:

5、组件动态化

所有组件,不能直接被业务模块构造,必须通过中台统一的工厂派发到对应的ComponentBuilder来构造,ComponentBuilder支持外部替换成自己的Builder,从而实现可动态替换组件的能力。

伪代码如下:

//创建默认的内部组件
ComponentFactory.buildDefault(AComponent.class);
//支持外部设置一个组件自己的构造器,时间动态替换内部组件
ComponentFactory.hook(AComponent.class, customBuilder);
//支持外部添加一个组件自己的构造器,时间动态新增内部组件
ComponentFactory.add(AComponent.class, customBuilder);
//创建一个组件,默认使用默认的Builder构造,如果被hook,使用hook的builder来创建
ComponentFactory.build(AComponent.class)

6、组件解耦机制

针对中台需要的框架模式,我们针对两方面进行思考与设计: 1、组件之间怎样实现零耦合,解除后如何通信? 2、每个组件都有能单独被业务方引入的诉求,组件怎样能够单独抽离可用?

目前业界有一些主流的解耦机制

  • 首先看下Arouter的模型

Arouter中的IProvider是比较典型的接口解耦的方式,如果中台的组件来套用的话,一般流程是:将组件的接口下沉到base通用层,下沉后,接口方法和数据对象彼此可见,在注册服务后,组件就可以使用彼此的功能了。这种形式比较方便简单的,而且更新方法和数据对象之后,可以通过报错的形式被通知,保证安全。

  • 再来看下Redux模型

redux不是严格意义上的为组件之间解耦的框架。Redux的核心思想是数据驱动,通过数据和事件将view和业务流程解耦,将不同的业务流程相互解耦。以应用的数据为应用的核心,通过事件产生数据变化,通过数据驱动view的展示。 这种思想其实也是可以应用到终端的,各组件对数据中心关心的数据进行监听,通过数据驱动来解耦。

可以看到这2个框架模型其实有个共性,就是中心化:接口中心和数据中心

如果中台使用接口下沉的方式,面临2个问题: 1、中台拆分后组件有将近100个,这么多组件接口以及相应的数据结构如果下沉到base,通用层会非常膨胀,将完全不利于组件的单独抽离使用。 2、模块依赖能力与组件接口提供不是完全匹配的

那么 redux这种数据驱动呢? 如果全部使用通用化数据结构是可以的,如传json,map这种字典,通用化数据结构在前端是应用比较成熟的。如果放在中台呢? 对业务方及共建者将是个易用性极差的存在,因为每个组件都可能要被业务方单独拿出去二次开发和替换的,字典的可维护性是相当差的。如果不用通用化数据结构呢,那么又面临前面一样的问题,数据结构需要下沉,base膨胀。 还有一个问题: 如果某个组件是对安全性要求较高的,它的部分功能可能是不希望随便对其他部分可见的,这个时候显然下沉不是一个好的选择。

1、中心化不适用中台; 2、业务之间的接口依赖也是不适用中台的,因为每个业务模块或者业务组件都可能被单独抽离使用,如果A依赖B的接口,难道A被拿出去时还要带着B的接口吗?

为此,我们开发了更适用于中台的解耦模式:胶水适配器 + 微中心

一个组件的Interface定义了对外的能力接口。 我们又给组件加了一层接口:Adapter 。 它的作用是定义对外需要的能力。2个接口各司其职:一进一出。

这个adapter其实就是这个组件的适配器,是组件赖以生存的原料。 1、我们在组件构造器中加入一层胶水,来完成对组件的适配,有了这层适配器,组件的使用和生存环境变得非常灵活,我们可以在其中加入一些复用价值低的组装逻辑,这里也是一种动态代理模式,业务方也可以灵活将代理转向自己的业务环境来适配组件; 2、我们针对业务组件全部去除了直接的接口依赖,组件只定义自己关心需要的内容,而不是直接获取接口了。针对使用非常频繁通用的接口(如上报,日志等),我们保留了一份微中心,只包含少量的基础组件。

整体结构如下:

这里给出一个消息服务组件的适配器示例:

MessageServiceInterface messageServiceInterface = new MessageService();
messageServiceInterface.init(new MessageServiceAdapter() {//消息服务适配器
            @Override
            public ChannelInterface getChannel() {//通道能力
                return serviceManager.getService(ChannelInterface.class);
            }
            @Override
            public DataReportInterface getDataReport() {//数据上报接口能力
                return serviceManager.getService(DataReportInterface.class);
            }
            @Override
            public HttpInterface getHttp() {//htpp请求能力
                return serviceManager.getService(HttpInterface.class);
            }
            @Override
            public long getAccountUin() {//获取当前用户的账号UIN
                return serviceManager.getService(LoginServiceInterface.class).getLoginInfo().uid;
            }
            @Override
            public long getAnchorUin() {//获取当前主播的UIN
                return serviceManager.getService(RoomServiceInterface.class).getLiveInfo().anchorInfo.uid;
            }
});

这个模式不是最优秀的解耦方案,但是却是非常使用中台“台情”的~ 优点: 1、不同业务组件完全解耦,包括对接口的依赖,组件可单独抽离使用 2、在复杂的业务定制场景下,组件可用性极大加强


五、框架中的痛点与解决

痛点1、膨胀化的直播房间页面,UI层难以管理,模块增减代价大

直播房间是个业务相当多的页面,并且会随着业务膨胀。 1、当我们将布局全部写在UI层时,会造成布局文件迅速膨胀,当要删减模块时,代价非常大; 2、管理混乱,不同的共建者往布局里添加布局后,会造成层级难以管理,UI性能差

页面模板化:

为了轻松管理房间布局及解决层级问题,我们将每个页面实行模板化。 第一步:层级规范 我们将每个页面都分为3层:顶层,业务层,底层 顶层:只放一些豪华礼物动效,状态stateUI 业务层:放各个业务模块的UI 底层:音视频渲染模块

第二步:按层级注册模块 业务模块索引自己对应层级,防止扩层UI滥用

第三步:布局下沉 每个页面无真实布局,只留有业务的坑位,通过业务模块组装将坑位指向需要它的UI组件,布局全部下沉到UI组件中

统一的模板组装框架

我们将页面划分成一个一个模板

1、每个模板有个模板配置,模板配置包含层次布局和层次模块注册; 2、一个注册模块的原子单位我们称之为Module,一个模板配置由不同的Module原子单位自由组装; 3、一个页面可以对应多个模板,框架会将对应层次的根布局给到注册到对应层级的模块原子单位 4、模块拿到对应ViewStub坑位后选择性交给一个UI组件去填装 5、UI组件拿到坑位后内部填充自己的业务布局

在Android里:

一个page对应一个Activity,一个模板对应一个Fragment或一个View,一个模板对应N个Module原子,我们通过jetpack的LifeCycle与模板原子生命周期绑定。当随模板启动后,Module有相应的生命周期,Module也会随着模板的销毁而自动销毁。 这样我们能轻松实现界面的动态变化。

例子:当我们切房的时候从一个直播房间切换到了一个小视屏房间,只要换个模板启动就OK了 相应代码示例:

//选择一个UI组件提供槽位置
getComponentFactory().getComponent(AnchorInfoComponent.class)
                .setRootView(getStubRootView())
                .build();
//UI组件内部索引自己的布局
@Override
public void onCreate(ViewStub rootView) {
      super.onCreate(rootView);
      rootView.setLayoutResource(R.layout.anchor_info_layout);
      rootView.inflate();
}

有了模板配置,原子单位注册后,模块的UI才会绘制,对应的流程才会启动。当我们要去除一个模块或新增一个模块时,只需要剔除相应的原子单位或者新增一个原子单位就可以了。

模板化页面的技术收益

1、利用模板原子轻松组装不同的房间类型页面

2、层级维护可控,针对UI组件自动化检查和提单 由于具体的布局都下沉到了具体的UI组件,我们在构建时可针对UI组件做自动化层级检测,我们针对每个组件分配了默认的维护人,当一个UI组件>3层时提单给对应的负责人。

这样我们的层级会有一个比较好的保障:

模板化页面的业务收益

我们将模板分层中的业务层布局和业务模块注册开放出去,这样业务方可灵活定制组装自己的页面,做到更纯粹的模块管理和扩展性。 代码示例:

public class CustomAnchorRoomBizModules extends AnchorPortraitBootModules {
    /**
     * 中间业务层布局,用于各种业务,可以只定制这一层
     * @return
     */
    @Override
    protected ViewGroup onCreateNormalLayout() {
        return  (ViewGroup) LayoutInflater.from(this.context).inflate(R.layout.custom_room_layout_audience, null);
    }
    /**
     * 业务层模板添加相关的module
     * @return
     */
    @Override
    protected void onCreateNormalBizModules() {
        //这个为业务自己的module
        addNormalLayoutBizModules(new CustomAnchorInfoModule());
        addNormalLayoutBizModules(new CustomPrepareUpLeftModule());
        //下面为sdk已有的module
        addNormalLayoutBizModules(new RoomAudienceModule());
        addNormalLayoutBizModules(new MiniCardModule());
    }
}

定制示例:

痛点2:组装层职责大,灵活性太强,业务复用性差

我们前面讲到,将业务模块分成了2大组件UI组件和服务组件,由胶水逻辑串接了2个组件,这里单模块内其实是个MVP的架构,组装层相当于presenter

这里面临几个问题: 1、胶水和业务逻辑交杂在一起,module内逻辑有变动时,module层基本不可复用 2、业务逻辑和视图组件紧密耦合,基本不可复用,组件在开发中的定位模糊 2、组装层权限很大,灵活性太强,可持续性差 示例:当我们UI组件业务方有定制更改时,由于胶水层是与UI组件紧密联系的,这层组装层基本不可复用。

我们先来看下我们的诉求是什么?以及现状和问题是什么?

预期

现状

1、module逻辑拼装层边界清楚,功能清晰明了

类似于MVP的Presenter,万金油能力,能拿到UI组件和服务组件,能写逻辑

2、业务逻辑在不同模块可复用

模块内部逻辑和模块的组装和组件有耦合,业务逻辑和胶水逻辑搅在一起,复用价值极低

3、业务逻辑接入方可灵活复用重写

得重写整个拼装模块和胶水逻辑

以主播信息模块为例: 组装层包含以下业务逻辑和拼装胶水,其中获取关注状态,关注请求和监听关注状态改变 这2个业务逻辑明显是可以被其他模块复用的,如结束页模块和资料卡模块

优化思考: 1、制定内部规范–明确指定module的职责 2、进行进一步拆解

单模块内架构优化 这里我们采用了MVP-Clean架构,细化职能,抽离胶水逻辑和可复用逻辑代码,将功能逻辑抽离成一个一个小的可复用片段

1、抽离后,弱化了拼装层职能,拼装只负责代理UI的Action,衔接数据回调刷新UI 2、功能逻辑层专职管理业务逻辑,并且内部的逻辑片段粒度非常小,可统一化输入输出接口

UI数据驱动: UseCase执行后,统一生成一个基于LiveData的State模型,监听者为执行者module。UseCase会产生一个UI状态临时数据。

protected abstract void executeUseCase(Params params);

public void execute(LifecycleOwner lifecycleOwner, Params params, BaseObserver<T> observer) {
      liveData = new MutableLiveData<T>();
      liveData.observe(lifecycleOwner, observer);
      executeUseCase(params);
}

为什么不直接让UI组件绑定数据? 还是前面提到的,我们UI组件的结构体是自己内部定义的,这样可以单独拿出去用,任何模块都不用依赖一个数据base。我们通过胶水层来代理转义,将数据层的结构体转换成UI层的结构体。

单模块整体模型,数据单向流:

我们在创建获取一个UseCase时,会将模板原子与UseCase通过LifeCycle绑定生命周期: 这样我们业务逻辑层可以不用关心生命周期,行为及ViewModel会跟随LifeCycle监听者一起结束。监听者就是模板原子,它是随着页面模板结束的,这样保证了我们的全局生命周期稳定性。

拆分后的技术收益 1、拆分后,中台模块的代码可复用和替换率极大提升

UI组件,服务组件,基础组件,逻辑片段,都可以我们可以复用和重写的模块。

拆分完成后,如果根据代码函数统计,可复用率可达75%

当然,这里只是一个预估值,这部分仍在持续拆分中~

2、在组件、逻辑拆分清晰后,可复用性高,单侧覆盖全,配对责任人,主流构建会定时跑单侧邮件输出单侧覆盖率,极大提升质量

痛点3:拆分后服务组件非常多,难以管理

面临问题: 1、逻辑层怎样获取数据服务? 2、数据服务非常零散,放到逻辑层创建管理非常混乱,该怎样统一管理? 3、数据服务需要有稳定的生命周期,例如房间销毁后,需要停止接收房间push,停止发送房间心跳等。 4、服务是有不同的生命周期维度的,某些服务又需要有同样的生命周期来维持整个系统稳定。 5、一组服务的生存环境是依赖一组关键流程启动的:如账号服务、通道服务等依赖登录,房间内的主播信息、成员列表、礼物等是依赖进房的

根据服务的生命周期和作用 ,我们其实是可以对这些服务划分作用范围的,根据直播特性,可分为以下几种:

Service管理设计思考 划分作用范围后,我们可以针对每个范围的服务加一层管理层,我们称之为引擎层,为什么是引擎?因为它既是一组服务的发动机,也是创建和管理服务的地方。 根据服务特性,添加三个引擎:LiveEngin、UserEngine、RoomEngine

LiveEngine:与整个直播场景生命周期一致 UserEngine:与用户账号生命周期一致 RoomEngine:与直播间生命周期一致

Engine包含2部分:

EngineLogic:负责引擎的环境启动,如用户引擎中负责登录创建通道,房间引擎负责房间的进房心跳环境 ServiceLoder:负责服务的生命周期和管理

有了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的生命周期,这些全都由框架来保障

技术收益 1、服务管理清晰可维护性强,生命周期安全可靠。 当我们去切换一个房间时,只用destroy上一个RoomEngine,启动新房间的RoomEngine即可 当我们去切换一个账号时,只用destroy上一个UserEngine,启动新用户的UserEngine即可

框架可轻松支持房间多实例,用户多实例等复杂场景。

2、接入方可轻松使用服务层搭建自己的直播应用

对于想从服务层接入的业务方,可以轻松利用我们的这套引擎层来快速搭建开发自己的应用。


六、iLiveSDK的整体框架

iLiveSDK目前演变为以下架构

七、iLiveSDK的接入现状

目前直播中台已接入上线以下平台,为业务加入直播变现、直播带货、直播交友玩法等助力。

我们建设了比较全的iwiki文档,其中包含接入和共建的一些开发指引,也欢迎大家加入共建。

最后附上直播中台iWiki地址

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、直播中台的诞生背景
  • 二、直播中台SDK的前期调研与分析
    • 1、直播中台具备什么能力?终端SDK的定位是什么?
      • 2、终端SDK需要具备什么能力?对外是怎样的形式?
      • 三、中台SDK框架面临的困难和挑战
      • 四、SDK框架方案探索
        • 1、页面层级的高粒度业务模块拆分
          • 2、业务模块内的精细化组件拆分
            • 3、基础模块何去何从
              • 4、组件标准化
                • 5、组件动态化
                  • 6、组件解耦机制
                    • 为此,我们开发了更适用于中台的解耦模式:胶水适配器 + 微中心
                • 五、框架中的痛点与解决
                  • 痛点1、膨胀化的直播房间页面,UI层难以管理,模块增减代价大
                    • 痛点2:组装层职责大,灵活性太强,业务复用性差
                      • 痛点3:拆分后服务组件非常多,难以管理
                      • 六、iLiveSDK的整体框架
                      • 七、iLiveSDK的接入现状
                      相关产品与服务
                      云直播
                      云直播(Cloud Streaming Services,CSS)为您提供极速、稳定、专业的云端直播处理服务,根据业务的不同直播场景需求,云直播提供了标准直播、快直播、云导播台三种服务,分别针对大规模实时观看、超低延时直播、便捷云端导播的场景,配合腾讯云视立方·直播 SDK,为您提供一站式的音视频直播解决方案。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档