直播与短视频相继爆发,也促使众多企业纷纷加入其中,对于许多传统企业和中小企业而言音视频开发成为了最大难点,而视频云客户端SDK也就无疑成为了不错的选择。本文是全民快乐研发高级总监展晓凯在LiveVideoStackCon 2017上分享的整理,主要从架构设计、模块的拆分实现、跨平台视频处理系统和推流系统的构建几部分着重介绍。
演讲 / 展晓凯
整理 / LiveVideoStack
我是来自全民快乐的展晓凯,曾就职于淘宝开发机票搜索,在唱吧上线之初加入,经历了唱吧从上线到拥有4亿用户的整个过程,在此期间负责唱吧音视频的开发,其中涉及多个产品线,包括唱吧、唱吧直播间、火星等产品。目前在全民快乐负责直播产品线业务,主要面向海外市场。
在唱吧和全民快乐多年的音视频技术积累,展晓凯也在近期发售了业内第一本音视频移动端开发书籍《音视频开发进阶指南——基于Android和iOS平台的实践》。(文末有彩蛋)
本次分享将从以下几部分来介绍视频云客户端SDK的设计与实现:音视频领域的发展,SDK的核心应用场景,视频录制器和视频播放器模块的拆分,跨平台视频处理系统和推流系统的构建,以及未来的机遇与挑战。
视频云客户端SDK发展和核心场景
音视频架构与开发的演进历经很长时间,大致可以分为以下几个过程:最开始是广电领域,也就是给电视台提供直播以及转码等服务;后来扩展到了PC端的音视频领域;而近几年则是在移动端音视频领域发展比较火热。
对每一个视频云厂商,除了提供持续、稳定、高可用的线上服务外,它其实也提供了客户端的SDK,以方便客户在不了解音视频细节的条件下,也可以快速构建自己的APP,这样也可以更加关注与自身所在垂直领域相关的业务。
那么SDK的核心场景有哪些?为了方便讲解,我们把SDK核心场景分为录播场景和直播场景:对于录播场景,主播端或者内容贡献者需要录制一个视频,后期对视频和音频频添加特效,比如主题、贴纸、混音、BGM等等,最终把视频上传到服务器,观众端则需要使用播放器播放以及社交互动即可;而对于直播场景同样包含这两个角色,主播端需要将内容进行实时直播,并针对于观众的一些行为完成实时互动,观众端则需要使用定制的播放器观看,这个场景下的播放器并非使用系统提供的播放器即可,必须加以定制化。
针对于录播和直播两个场景,他们的共同特点都包含视频录制器和视频播放器;区别则主要体现在是否具有实时交互性;他们需要在各自场景下做一些特殊的配置,比如对于直播来说推流的稳定性和拉流的秒开,对于录播则是后期视频处理和上传。
视频录制器的架构设计
模块拆分
视频录制器分为三部分:输入、处理和输出。输入就是通过摄像头和麦克风这类采集设备去做音频和画面的采集。处理则是针对采集到的画面和声音进行处理,比如大家熟知的美颜、回声抑制、混响等等。最终输出会分为几部分:首先是预览,比如用手机录制视频时,在屏幕上会有预览画面;第二部分是编码,在安卓平台采用硬件编码+软件编码,而iOS平台的兼容性较好,所以只采用硬件编码就可以达到要求;最后将音视频数据封装成一个容器——FLV或MP4,再进行IO输出,IO输出有可能是磁盘——录播场景,也有可能向流媒体服务器推流——直播场景。
音频架构设计
上图是音频架构图,由于Processor比较复杂,因此在里面没有做体现。从图中可以看到,音频架构分为Input、Output、队列和Consumer几部分,架构图上下部分分别是安卓平台和iOS平台实现的结构。
用户在K歌过程中需要混入伴奏音乐,对于安卓平台而言,需要有一个MP3的Decoder,它可以通过MAD、Lame或者FFmpeg等开源库来实现,最终通过AudioTrack 的API或者OpenSL ES的API来播放,同时我们把播放PCM数据放到PCM队列中。而在采集过程,我们一般使用Audio Recoder或OpenSL ES来采集人声,采集到的人声也会放在一个PCM队列中。在一般架构设计中,队列一般承担生产者和消费者中间解耦的角色,因此可以看到Input和Output就是上面两个队列的生产者,而Consumer线程中的Encoder就是消费者——从队列中取出PCM数据进行编码。
对于iOS平台,我们使用的AUGraph,它底层使用的是AudioUnit,其中RemoteIO类型的AudioUnit可以采集人声,AudioFilePlayer类型的AudioUnit可以播放伴奏。然后通过Mixer类型的AudioUnit将人声和伴奏混合之后入队,后面Consumer线程中的Encoder从队列中取出PCM数据进行编码。
视频架构设计
视频部分的结构设计相对会简单一些。安卓平台通过Camera采集视频,在Output中首先是通过EGL Display来回显预览界面,其次编码则是采用MediaCodec硬件编码和Libx264软件编码相结合的实现方式(由于安卓平台硬件编码有可能出现兼容性问题)。
而在iOS平台则会更简单,直接使用Camera采集,然后通过GLImageView来进行渲染——GLImageView的实现方式是继承自UIView,在LayerClass中返回CAEAGLLayer,然后构造出OpenGL环境用来渲染纹理,最终再用VideoToolbox进行编码输出。编码后的数据会放到H.264队列中,那么这里的生产者就是编码器,消费者实际上是Consumer模块,它把H.264队列中数据Mux后再进行IO操作(输出到磁盘成为mp4文件或者输出到流媒体服务器)。
视频播放器架构设计
模块拆分
视频播放器的模块拆分和视频录制器非常相似,同样分为输入、处理和输出三部分。首先是IO输入——本地磁盘或远程拉流,拿到码流后需要进行解封装(Demux)过程,也就是封装(Mux)的逆过程,它会把FLV中音频轨、视频轨以及字幕轨拆解出来,然后进行解码过程,一般采用采用硬件+软件解码的方案。
视频播放器中中间处理过程使用的并不算很多,音频处理上可以做一些混音或者EQ处理,画面处理则是画质增强,如自动对比度、去块滤波器等,当然播放器处理中非常重要的一环就是音视频同步,目前一般有三种模式:音频向视频同步、视频向音频同步以及统一向外部时钟同步。我们一般会选择视频向音频同步,这主要是由于两方面的原因:一方面是由人的生理特性决定的,相比于画面,人对声音的感受会更加强烈;另一方面音频PCM是线性的,我们可以信赖播放器也是线性的播放,因此在用视频帧向音频帧同步时,我们允许对视频帧进行丢帧或重复帧渲染。最后,输出则主要包含音频渲染和视频渲染两部分。
运行流程
对一个多媒体文件,视频播放器会对其进行Demux和Decode处理,当解码器解码出一帧视频后给到队列,这时如果是软件解码则一般解码出来的是YUV格式,然后放入到内存队列中;如果是硬件解码则一般是显存中的纹理ID,会放到循环显存队列中。解码出音频的PCM数据也会入队。
对于这两个队列来说也同样存在生产者和消费者,解码器就是生产者,右边的Output则是消费者。这里值得一提的是,可以通过设置两个游标值来做队列的控制——minSize和maxSize,当队列中的音频大小到达minSize时,消费者则会开始工作,而当音频大小到达maxSize时,解码线程就要暂停工作(wait住),当消费者消费了队列中的内容后,队列中音频大小小于maxSize的时候,会让解码线程继续工作(发出Singal指令)。而消费者的工作流程为:从音频队列中取出一帧音频帧给音频播放模块进行播放,然后会通过AVSync音视频同步模块取出一帧对应的视频帧给视频播放模块进行播放。当生产者、消费者周而复始的运转起来,整个播放器也就运行起来了。
音视频同步策略
前面提到我们音视频同步策略是采取视频向音频同步,也就是说假设我们在播放音频第一帧时,对应的第一帧视频没有过来,而此时马上要播放音频第二帧,那么我们就会选择放弃第一帧视频,继续播放第二帧从而保证用户感受到音视频是同步的;那么假设当没有播放第三帧音频时已经接收到对应的视频帧时,则会将视频帧返回,直到对应音频播放的时候再取出对应的视频帧。
那么对于普通开发者而言,想要实现播放器每一个细节其实是非常复杂的,尤其对于一些创业公司或者对于音视频积累比较薄弱的公司来说,所以直接接入CDN厂商提供的SDK是不错的选择,这样可以尽快实现自身业务逻辑,而伴随着业务的发展,后期可以针对特殊需求基于SDK进行二次开发。
从个人经验来讲,我认为SDK中技术含量较高的主要有两点:跨平台的视频处理系统和跨平台的推流系统构建,接下来我会做重点介绍。
跨平台的视频处理系统
跨平台的视频处理系统实际可以说是跨平台的图片滤镜系统,它所应用的场景主要有实现美颜、瘦脸这种单帧图片的处理,也有如雨天、老照片等主题效果,以及贴纸效果这几种。为了达到效果,我们通过OpenGL ES来实现,如果用软件(CPU中计算)做视频处理是非常消耗性能的,尤其在移动端无法接受。因此它的输入是纹理ID和时间戳,时间戳主要用于主题和贴纸场景的处理。输出则是处理完毕的纹理ID。
GPUImage
这里特别介绍下GPUImage框架(以iOS平台作为讲解),它的整个流程分为Input、Processor和Output。首先通过GPUImageVideoCamera采集画面;然后转化为纹理ID就可以通过模糊、混合、边缘检测、饱和度等一系列处理进行优化;最终Output中使用GPUImageView把处理完的视频帧渲染到屏幕上,而对于录制则提供了GPUImageMovieWriter,它可以将纹理ID硬件编码到本地文件。除了视频录制过程,它对视频播放器和离线处理场景提供了GPUImageMovie作为Input的实现。
跨平台的视频处理系统构建
对于搭建跨平台的视频处理系统,我们需要搭建两个客户端的OpenGL环境,安卓平台使用EGL来提供上下文环境与窗口管理,iOS使用EAGL来提供上下文环境与窗口管理,然后我们抽象出统一接口服务于两个平台。
这是结构图,左边是根据平台搭建的环境——Platform OpenGL Environment,右边是视频处理系统—VideoEffectProcessor。整个过程为:首先通过Camera或者Decoder采集或者解码出视频帧纹理,将纹理ID交给VideoEffectProcessor完成视频处理功能,而这里面可能需要很多支持,比如集成一些第三方库解析XML、解析Json、libpng等等,同时我们也要暴露一些可以动态添加和删除Filter的功能。当处理完成后会输出一个Output TexId做渲染,最终呈现到界面上,或者给到Encoder做离线保存。
跨平台的推流系统
我们先来看跨平台推流系统的应用场景,首先无论网络是否抖动都要维持交互的实时性,其次要保证正常直播的流畅性,并能根据网络条件的好坏来决定清晰度,最后要有统计数据来帮助产品运营做策略优化,比如提升码率、分辨率等等。针对这三点场景分析,如何从技术角度实现?首先在弱网下做出丢帧,第二是码率自适应,第三为了保证主播端持续直播,需要做到自动断线重连。
那为什么要做跨平台的推流系统?这主要考虑到开发成本和效率的问题,从开发策略制定和测试的角度来看都可以节省一部分成本,而且一套代码在后期维护中也有很多好处。那么跨平台推流系统应该如何实现?我们使用FFmpeg将AAC和H.264封装成FLV格式,然后使用RTMP协议推到流媒体服务器上就可以。
弱网丢帧
当检测到H.264或AAC队列的大小超过一定域值时,我们要做丢帧处理,因为此时可能会导致现在的数据很长时间发不出去,从而交互的实时性就无法得到保证。当我们需要进行丢帧处理时,对于视频帧要明确丢弃的是否为I帧或P帧;对于音频帧则有多种策略,可以简单丢弃与视频丢帧相同时间长度的音频帧。
码率自适应
对于码率自适应,我们需要检测上行网络带宽的情况,准确的说是上行网络到推流的流媒体服务器节点的情况。当需要降低码率,我们要把现在编码队列中高码率的视频帧丢掉,并让编码器强制产生关键帧,以保证最新的视频以低码率推到服务器上完成整场直播的交互性。改变编码器的输出码率,对于libx264来说,需要在它的客户端代码中改变vbv buffer size,并Reconfig X264编码器才可以;而对于FFmpeg的API则是需要改变rc buffer size,并且需要ffmpeg 2.8版本以上才能支持;对于MediaCodec和VideoToolbox则使用各个平台硬件编码设置。
这张图是通过当前发送的码率调整实际编码器产生的视频码率,这里调整的不仅仅是码率,同时也包括帧率。当帧率较低时,单纯提升码率也无法达到视频质量提升的效果,因此两者会一起做调整。
链路选择与自动重连策略
在链路选择方面,尤其在某一些特殊场景下,DNS解析不一定能找到最佳链路,我们可以选择直接接入CDN提供的接口,在主播推流前向CDN厂商请求一个最优节点,而不依赖Local DNS去解析IP地址;对于主播端,也可以POST一个500KB的flv文件,在多个推流节点测试网络链路情况,从中选择最优链路。再者推流一段时间后,网络链路有可能会出现拥塞的情况,IDC机房节点也有可能出现问题,因此SDK底层需要有自动重连机制来保证重新分配更优的链路和CDN节点,从而保证主播持续推流不受影响。
数据收集
最后是数据收集,数据收集涉及到后期调优、评判链路节点等等,因此非常重要,而这也是用定制播放器的原因。基本统计的点包括连接时长、发布时长、丢帧比例、平均速率、设置速率和码率自适应的变化曲线等等。
以上是本次分享的全部内容。
酷炫短视频开发进阶&新书抽奖
此外,我们还特别邀请了展晓凯在下周二线上与我们一同分享酷炫短视频开发的设计架构、实现思路以及研发过程中的经验。
在参与直播互动的小伙伴中,将抽出10位赠送展老师的新书《音视频开发进阶指南——基于Android和iOS平台的实践》,同时我们也会面向参与直播的小伙伴开放购书优惠通道。
扫描下方图中二维码,加入直播群。