短视频客户端SDK设计与实现

2021-09-02 11:34:30 浏览数 (1)

直播与短视频相继爆发,也促使众多企业纷纷加入其中,对于许多传统企业和中小企业而言音视频开发成为了最大难点,而视频云客户端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节点,从而保证主播持续推流不受影响。

  • 数据收集

最后是数据收集,数据收集涉及到后期调优、评判链路节点等等,因此非常重要,而这也是用定制播放器的原因。基本统计的点包括连接时长、发布时长、丢帧比例、平均速率、设置速率和码率自适应的变化曲线等等。

0 人点赞