本文由百度智能云音视频SDK产品技术负责人李明路在LiveVideoStack线上分享的演讲内容整理而成,内容从音视频数据角度出发,梳理了音视频SDK的发展和技术演进。详细分析数据在常见音视频模块上遇到的问题与挑战,并提出相应的解决思路和技术实践。
文 / 李明路
整理 / LiveVideoStack
视频回放:https://www.livevideostack.cn/video/online-lml/
本次分享的主题是移动音视频SDK开发工程实践,内容主要分为以下五个部分:
- 音视频SDK的技术演进
- 数据采集管线的设计与实现
- 特效模块数据中间件设计和实现
- 连麦模块数据中间件设计和实现
- 渲染模块数据中间件设计和实现
音视频SDK的技术演进
1.1 数字化进程
image多媒体技术是一项传统技术,但同时也是在不断发展与进步的,我们可以形象的用细胞分裂来表示,即多媒体技术内部也在不断发生分裂,而音视频SDK则是其中的一个分系。
以下大致列出了音视频技术演进的几个重要进展。首先,在互联网化前期多媒体技术就已经开始出现广泛的应用,得益于通讯技术的发展,我们可以借助于广播、有线电视、卫星等等,将数字信号传输给用户。
解码器其实在整个多媒体技术的发展当中也起到非常重要的作用,只不过当时解码器的能力与现在相比还是有些不同,但同样可以实现解码和容器的分离。
此后,随着互联网的出现,我们可以实现基于一些IP网络协议、光纤的广泛使用,通过WiFi、蜂窝网络,传输数据到设备终端(此时终端更多的还是指PC端)。因此产生了一些在线类的音视频服务,例如点播、在线语音等典型的音视频场景。在这些场景中,SDK更多的还是以服务端为主。
2010-2015年期间,随着手机硬件的发展,终端的算力不断提升,编解码芯片也得以快速发展,再加上消费者使用习惯的变化,出现了更多碎片化的场景和产品,像直播、短视频等。此时,随着手机等移动设备的普及,移动端SDK也慢慢进入到消费级领域,逐渐发展成独立技术栈。
近年来, 随着5G、AI人工智能的发展,VR/AR、语音交互等技术也在发生着新的变化,应用场景变得更加广泛,音视频SDK已经不仅仅局限于移动端,未来会出现在各种设备屏幕、产品形态当中。
1.2 移动端音视频框架
image移动端的音视频框架与其它移动端框架相比有很大不同。音视频框架首先要立足移动端提供的系统能力,包括系统框架和硬件能力。如图,系统框架层有iOS/Android的一些多媒体框架、硬件的编解码、硬件的处理能力如CPU、GPU、NPU的处理模块,以及一些开源图像库。
系统框架层之上是第三方框架,如FFmpeg、OpenSSL(加解密);在点播场景用的比较多的Ijkplayer/Exoplayer;用于底层通讯的技术:双向的,低延时的WebRTC技术,用于单向点播的RTMP,目前比较火的SRT低延时方案;除此之外,还会有一些图像处理方面的框架如GPUImage等。
在此之上,就是一些跟场景相结合比较多的模块,这里大致列出了部分核心模块。
数据从多媒体采集模块出来,会经过一路或多路的混音混流(与实际场景相结合),然后过渡到多媒体编辑模块:当下短视频的一些能力都是通过多媒体编辑这个处理单元实现,再到后面的多媒体后处理模块:例如AR特效,以及一些比较好玩的互动能力等;
内容生产完成后,我们会根据点播、直播或者短视频等场景的不同,采用不同协议进行分发。再下面就是消费侧,这里可以直接读取数据,也可以间接读取数据作为缓存加快二次读取数据的载入。获取数据后会根据不同的封装格式,如FLV、TS或者是MP4等对数据进行解封装,再进行解码和渲染操作。
相比与其它移动端框架音视频框架最特别的地方就是管线部分,因为音视频SDK的产品与其它产品不太一样,首先需要的是实时处理,数据流是不断在各个模块之间穿梭的,如何保证各个模块间的高效传输,这里提出了一个管线的概念。
1.3 唯快不破
image简单介绍了音视频SDK的框架,接下来介绍下目前框架遇到一些问题和挑战。目前很多业务场景,其实都在追求更多的新玩法。这里可能会有一些追求更大的分辨率,还有一些更高的刷新率,甚至是更极致的体验。但是我们知道音视频SDK很大程度上会受制于平台的能力,由于平台具有更多的差异性,所以导致音视频SDK在发展过程当中,其实遇到很多的问题。另外还涉及到模块间的数据交互,因此性能对于移动端来说是一个最大的瓶颈。
其实在有些场景像点播或短视频,720p的分辨率已经不能满足场景的使用,甚至在一些运动场景,30fps的刷新率也已经没办法满足场景的开发,当然这些其实更多的是业务挑战。除此之外,刚才也说过整个多媒体技术的发展,编码器一直起到了举足轻重的作用,所以底层技术也在追求更高效的编码效率。
对于这些问题,其实大家都能达成一个共识,就是说速度是音视频SDK框架要解决的一个问题。但抛开现象看本质,速度其实还是来自于数据的传输,它是一个根本前提条件,因此如何让数据更高效的传递和更高效的处理,才是移动端SDK要解决的最根本问题。
数据采集管线的设计和实现
image这里介绍一下目前一些开源产品当中数据链的设计和方案,以两个产品为例,一个是GPUImage。相信做过移动端SDK特效的同学应该都是耳熟能详了,因为它确实是比较好用,可以提供大量的Shader。抛开Shader的不说,我们看一下它在数据链条的设计方式。首先GPUImage提供了一个传输数据的类Output和一个实现接收数据的Input,然后通过生产模式,生产模块像相机采集模块,以及本地相册模块,其实都是实现了Output的类,生产模块生产完毕后会交由Output进行渲染,这里它的强大之处在于Output可以与可编程的Shader进行强关联,很大程度的驱动了OpenGL的运行效率。然后将渲染后RGB的数据转换成FrameBuffer这个实体的对象,整个链条都是通过FrameBuffer进行接收,然后通过链条上的组件去实现了Input后,就能拿到Output传出的FrameBuffer,以此类推将FrameBuffer进一步的传递到下个链条当中,整个链条其实是非常清晰和简单。
我们再来看一下右边的FFmpeg,除了提供很多的数据链条的处理机制、数据的分包,它还可以在不进行解码的情况下进行转包,也可以把数据进行解码,做一些后处理等等,整个链条其实也是比较清晰的。
image介绍了两种多媒体框架,接下来看一下我们团队是怎么去考虑这些问题。实际开发中,音视频SDK框架中管线其实是不可控的,我们来看一下这两种方案的一些优势和问题。
首先GPUImage的优势是显而易见的,就是协议相对简单清晰,另外通过可编程Shader驱动了OpenGL,这样它的运行效率变的非常快,通过大量的Shader可以让开发同学去进一步学习这门语言,是一个优秀的图像处理 开源框架。但同样存在一些问题,首先我们看到它提供的这些Shader其实大多数都是图像处理,并且基本上都是同步处理的。所以可能导致在使用GPUImage跟一些框架进行结合的时候,当你的某个模块涉及到耗时处理,这个时候有可能会出现某个对象被其它释放掉或怎么样,会造成整个线程的不安全。
FFmpeg的优势特别多,简单说一下它的函数指针,其实也是FFmpeg的一个最佳实践,它通过指针的方式,把数据进行一层层的拆分跟传递。但是它也同样存在一些问题,比如说FFmpeg是面向过程的,它的链路设计对于不熟悉FFmpeg的同学来说,可能会觉得链路非常复杂,使用起来可能并不是那么的得心应手。
所以我们经过多轮讨论跟思考,就希望得到一个数据管线,它首先像GPUImage,协议比较简单,用起来比较容易上手,对开发者来说是友好的;另外需要它是可控的,因为随着SDK的模块越来越多,我们发现在很多场景下,会出现不可控的问题;除此之外我们还希望它是支持同步或者是异步的,调度是安全的,链路配置是简单可依赖的。
image我们也总结了自己的一些方法论跟实践:专注链式,这主要还是考虑到音视频框架,链式是它的一个核心思想,所以我们遵从这种法则自研数据管线。
首先是制定数据协议,主要解决的是最基本的模块间数据的高效、稳定的传递。这里可以看到左图,它的方案跟刚才介绍的GPUImage有些相似,但是也有些不同。我们也提供了类似的AVOutput的模块和AVInput的数据传输接收的协议。但是我们并没有跟GPUImage的OpenGL进行绑定,只是单纯的去记录和管理这条链条上的各个的组件,我们叫target。然后通过Dispatcher的机制,分发从生产端上报的视频帧,通过视频帧不断的传递到各个链路的target当中,而每个target实现AVInput其中的协议方法。例如frame跟type两个函数,frame就是从raiseFrame Output传递下来的数据;type主要是做一些音视频的音频跟视频的区分。
除此之外我们还支持一些二进制场景的分发,主要是为了配合像直播这种对数据有分发要求的场景做了一些协议的升级。在链条的最末端,可以看到我们也实现了AVControl,跟之前不太一样是我们制定了一个控制协议,这个控制协议主要是为了控制数据的流入和流出。为什么要做这个事情呢?我们知道音视频SDK最核心的就是数据在不断的传递,如果说某个模块出现异常,数据没有一种机制能保护它,可能会导致整个SDK的运行出现不稳定的情况。比如在直播场景分发的时候,我们发现网络有抖动,此时我们可以调整发送的速率、速度等。
这里简单的画了一下我们的数据流向,一个是推的方式,就是直接从相机这种模块采集数据,直接着向后面的某个模块去传递。一个是拉的方式,主要是指读取本地文件或者说是间接的获取数据,比如从网络读取数据首先需要将数据读下来,然后再传递到各个模块。这里其实相对于之前说了GPU在异步线程的一些处理,我们也做了一些兼容和保护,就是防止异步处理时对象被释放的情况发生。所以说我们基本上也是沿用了GPUImage协议简单的这种思想,然后在此基础上又增加了一些像控制协议的实现机制,让整个链路变得可控。
image除此之外,我们也发现在日常过程当中,只是简单的设计数据管线,其实并不能很好的对业务场景开发提供帮助,原因什么呢?
其实我们只是解决了各个模块数据之间的传递问题,但是并不能对最终产品的场景开发落地解决所有的问题,因为场景跟模块是有很大差异的。我们所了解的像点播、直播,甚至是特效等,它其实都是通用性的能力。但是实际场景就涉及到各个模块之间的组合,所以数据的传递并不能说传递一个数据,就能把一个场景串联起来。
这里也举了几个日常过程中典型的例子,比如对于点播的画质优化,会发现类型转换不是那么通畅和简单。对于连麦场景,如何让我们的SDK或者产品用起来更简单,像人脸特效,比如说涉及到能力的多样化,如何做到兼容性等,这些都不只是靠数据链路或模块搭载就能解决的。因此我们提到了一个中间件的概念,就是把数据进行桥接实现资源的共享,在各个模块或业务输出或使用的时候,就能提高整体数据采集或处理的应用性。所以说应用性也是我们数据采集管线的另一个考量指标。
特效模块数据中间件设计和实现
接下来我们来看一下,在实际过程当中遇到的一些问题跟解决方案。
image特效模块通常是典型的PaaS结构,它上面存在多种模型,而且模型之间是可以进行插拔;它还有另外一个特点就是耗资源。那么如何在音视频SDK中将这个模块更好的运用起来,去对外提供能力呢?我们这里以人脸特效的高级美颜接口为例,高级美颜中涉及到的特征点非常多,像大眼、瘦脸、下巴等,而且这些特征点并不是通过一次迭代或是一个模型就能解决的,它可能会涉及到多次的迭代和多种模型的组合叠加。这样就会带来一个问题,在集成特效模块的时候,如果这些能力都是不断变化的,对于模块的使用来说,就存在一个不安全不稳定的因素。那么我们该如何把这个问题解决,或者说屏蔽掉呢?
这里我们提供了一个概念:首先在调用能力的时候,不直接去调用,而是把这些能力做进行抽象、封装,然后这些封装的模型,用来关联后面的一些不同的算法。因为别人在用SDK的时候,不一定按照你所有东西来集成,可能只使用其中部分能力,这样就有可能会导致一些特效的SDK存在版本不一致。如果没有这个代理层,当出现版本不一致的情况,对于上层来说可能就会出现大量接口的调整和修改,会比较耗时耗力。不过我们做代理的话,可能就给屏蔽上层接口的稳定性,然后通过我们的模型跟一些抽象对象,可以去驱动不同的AR模块的版本。
通过数据管线我们可以看到,从录制模块把数据传到effect接口,再把数据再给到AR SDK的时候,每个AR SDK都会有一个处理检测能力,会定时的检测主屏指标等,为什么要这么做?
我们知道现在的特效,场景、玩法是特别复杂的,处理效果没办法完全保证。所以为了保证每帧的处理和整体链路的稳定,我们需要不断做一些性能指标的监测,这些指标监测需要不断的反馈给上层的调用。比如当前数据传输的速度较快,或者传递帧太多处理不完,我们就可以通过数据管线进行回传,进行控制。甚至可以调整录制模块采集的帧率等,这样再把数据返回给录制模块,录制模块再将数据传递给其它模块,像预览进行渲染等。通过数据管线加代理的方案,我们就可以很好的整合不同AR版本、能力,对外保持接口的统一。
连麦模块数据中间件设计和实现
image连麦模块也是目前音视频产品中用的比较多的一个能力,如在线教育、直播带货、PK、娱乐等等,并且已经慢慢成为一个标准化能力,但连麦模块在使用过程当中还存在比较多的问题。
标准直播是单向通信,且信令相对简单。融合连麦模块后,则会变成双向通信,信令会变得复杂,媒体流从单路流变成了多路流。如左下图,一个标准的直播SDK加连麦SDK的整合结构,可以看到白色区域是标准的直播流程,从创建直播间到建链、编码、封包,包括通过队列进行分发等等。当融合了连麦能力之后,对整个模块来说会增加更多的链路。首先会引入RTC的服务端,媒体服务端与信令服务端。另外可能还会引入一些类似于业务系统的消息机制,IM服务器等等。
用户如果发起连麦,可以看到左图红色的箭头。首先它会向RTC信令服务器发送连麦请求,同时也会向IM服务器发送一个请求,向IM服务器发送请求的原因,主要是为了做一些业务上的处理,比如说UI界面或者场景的一些流程处理。实际上信令服务器主要是为了传递加入房间的请求,请求到达主播直播间后,主播直播间会响应信令服务器,选择同意或者拒绝。如果同意,则会通过信令服务器将信号返回给小主播/观众,小主播/观众这个时候就会把数据传递到RTMP的媒体服务器,主播也会把媒体流传到RTMP服务器,两路流汇聚到RTMP服务器后,通过旁路转播等方式来进行对外的转播,此时观众就会看到两个主播或主播和观众合流的画面。
这个实现流程其实比较复杂繁琐,对于用户(主播)来说可能理解起来非常吃力。首先他要关心的东西特别多,例如 IM服务器、旁路直播,旁路直播还会涉及一些模板的设置等等,他可能都要去关心,所以使用起来非常的麻烦。而对于消费侧的观众来说,也同样存在一些问题,比如说会出现相同地址,但是会出现不同编码的两路流,这种情况下可能对于一些播放器来说会有一些挑战,可能会出现一些卡顿,因为它在不断的重置一些编码参数,另外这种方案通过旁路转推的方式,可能原始的直播流会断掉,就在频繁切换直播流跟混合流的过程当中可能就会出现延迟,可能对于拉流侧来说它就有很多的问题,也不太好。
针对这些问题其实我们也看到,就是用户在使用过程当中其实有很多的不方便,所以说可以看到说简单的数据传递,其实并不能让这个场景做的特别的简单和应用,后面我们就做了一些大量的端到端的一些数据链的整合,首先是我们考虑到了就是消费侧的编码播放器的一些问题,首先是切换编码参数,这个方案可能并不是一个适用的方案,所以我们采用了本地混流的技术方案,其好处在于我的推流各方面的参数可以保持一致而不发生变化,而其实对于移动端的处理能力来说,本地的2~3路流的合流,其实对一些硬件来说,压力并不是特别大。另外我们把IM服务器跟RTC信令服务器进行了整合,因为我们觉得让用户去关心这么多的信令其实是没有必要的,而且用户也可能会被这些问题所困扰,所以我们内部就通过服务端的方式进行了消息的整合,这样子就让用户使用起来变得更加简单。通过这种端到端的一些媒体和信令的整合,其实也是让数据流变得简单,提高了整体接入的应用性。
渲染模块数据中间件设计和实现
image为什么说渲染是音视频SDK关键技术之一?从整体技术链路的角度来说,渲染模块实际上是用户最能感知到的一个模块。在一些复杂的场景下,渲染模块也承载了一些数据的交互和处理。因此渲染模块所包括的处理已经不再只是渲染,可能会涉及到某些场景的特殊需求,例如清屏等等:多人会议的时候,相当于一个关闭画面的效果;如多路混流,前面提到的RTC混流方案等等,渲染模块也可以作为其中的一个技术方案。也包括像小程序,尤其是一些小程序、安卓的同层设计方案,渲染方案等等。
每个模块甚至每个处理的节点,都有可能存在渲染的需求,所以说我们要将渲染模块进一步拆分。首先通过数据管线的一个能力,把各个模块的数据汇聚到渲染模块,然后再进行数据的处理。这里通常会将一些数据转换成各个平台的处理能力,比如CPU的Buffer,还有一些Surface等等。
数据加工,相对之前的生产流程这是新增的一个节点,它主要是为了应对一些复杂场景,如安卓的同层渲染、Surface的创建与绘制相分离,比如业务模块持有了Surface,但是渲染模块会间接引用并绘制。这里可能还会去做多路流混流的一些参数化配置,可以把每路流作为一个间值进行保存,也会做像图层跟视频帧的绑定,然后通过渲染线程同时绘制多路流。
我们都知道渲染必须要独立于当前的线程,否则对CPU和整体的开销影响还是比较大的。因此在创建完GL的环境之后,会按照GL队列将数据进行遍历拆分,来实现单路流,甚至是多路流的绘制。接下来就是一些标准的渲染流程,包括设置渲染的顶点,取渲染纹理,然后新增混合模式,通过绑定纹理来实现OpenGL整体的绘制。
网友评论