前段时间因为项目需要,我探索了一下如何在iOS上支持dash视频,现在分享出来给大家参考。
dash是什么
DASH,又叫MPEG DASH,DASH:Dynamic Adaptive Streaming over HTTP ,是一种在互联网上传送动态码率的Video Streaming技术,类似于苹果的HLS,DASH会通过media presentation description (MPD)将视频内容切片成一个很短的文件片段,每个切片都有多个不同的码率,DASH Client可以根据网络的情况选择一个码率进行播放,支持在不同码率之间无缝切换。YouTube采用DASH。其网页端及移动端APP都使用了DASH。DASH的其他采用者包括:Netflix, Hulu。
那为什么要使用dash呢?主要还是因为我们使用的AWS的服务,而且我们的音频使用了G711,而AWS的HLS格式不支持G711,只有Dash和mp4支持G711,因此不得不用Dash。而众所周知,苹果系统的音视频框架不支持Dash,因此只有借助第三方的库和框架来支持。
方案对比
我初步搜索了一下,目前支持Dash的第三方框架主要有阿里的Cicada、VLC以及FFmpeg(网上说4.2之后就可以,最好是用最新的)。先说结论,三个框架我都试了下,发现Cicada的解析速度是最快的(从发起请求到出图大概2~3s左右),但是对于G711似乎并不支持,而VLC和FFmpeg解析慢一些(ffmpeg大概7~8s,VLC大概10~12s),但都支持G711.
方案一、FFmpeg
由于项目中已经有使用FFmpeg,只是版本较低,因此我首先考虑使用FFmpeg,在把版本升级到最新的7.0之后发现,ffmpeg存在严重的性能问题。 首先是出图的速度慢,从连上服务器到第一帧图像出来,平均耗时7~8秒,其中单avformat_open_input函数就花了6~7s,其次,由于我们的dash切片大概是2s一段,ffmpeg每次拉取新的切片都是实时调用av_read_frame,这个又要花大改1~2s的时间,因此会造成很明显的视频卡顿。我在网上搜了些方法,基本都只有优化avformat_find_stream_info这个函数的,通过减少probe_size和指定inputFormat,大约可以减少0.5s的耗时,对于整体而言帮助不大。因此,我决定尝试其他方案。
方案二、Cicada
基于对阿里的了解,我一开始对Cicada不抱很大希望,因为很多项目到后面都没人维护,但看网上介绍的比较多,我还是尝试了一下。一开始我用的是直接编译出动态库,然后拖到Xcode里面使用,但是发现没有声音,也就是说不支持G711。我到Github上搜索了一下,发现没人提issue,于是便提了个issue,同时用他的demo调试一下,看看到底是什么原因。后面发现他内部在选择解码器时候,总是会选择苹果官方的解码器,这明显啊的不支持嘛,于是再往里面看,发现在ffmpeg_Utils这个文件里面有一个函数,
enum AFCodecID AVCodec2CicadaCodec(enum AVCodecID codec)
和一个table
static codec_pair codec_pair_table[]
只要在这里面添加G711对应的格式就可以让他选择ffmpeg的解码器了。 但是,事情往往没有这么顺利(不然我也不会写这个文章了~),我改完之后跑了一下,发现还是没声音,然后又继续排查,一直查到avcodecDecoder这个类的init_decoder函数里面,
int avcodecDecoder::init_decoder(const Stream_meta *meta, void *wnd, uint64_t flags,const DrmInfo *drmInfo)
发现在调用avcodec_find_decoder这个函数时,传入的codecId为AV_CODEC_ID_PCM_ALAW,没有返回对应的codec
mPDecoder->codec = avcodec_find_decoder(codecId);
这就很奇怪了,按道理说G711是ffmpeg很早就支持了的,为何会找不到呢? 我怀疑是ffmpeg这个版本有问题,我查了下,Cicada0.44版本使用的是ffmpeg4.3.1(可以在Cicada的external/external目录下找到依赖的第三方库),而这个版本在官方网站上已经不推荐了,会不会是有bug?带着疑问,我尝试升级其版本(修改external/player_git_source_list.sh中的ffmpeg版本),却发现升级后在编译Cicada SDK时各种报错,感觉是跟某个系统的宏定义冲突了。看来暂时没法升级,只能作罢。
虽然G711不能用,但我还是好奇为什么Cicada比其他两家快,因此研究了下他的下载流程,发现他用到了多线程并行下载音频和视频流,从下面的日志截图可以看出,两者几乎是在同一时间进行的。
加载视频的时间差不多1S
从开始访问服务器到出图差不多花了3S
方案三、 VLC
对于大名鼎鼎的VLC,我还是抱有很大希望的,尽管从VLC已编译版本的表现来看,依然存在跟ffmpeg类似的出图慢问题,但至少后续播放过程中没有出现的卡顿,因此可以考虑替换掉ffmpeg。 VLC官网 VLC github
VLC在cocoapod上还有一个编译好的库,叫mobileVLCKit,可以直接用,省去编译的麻烦,如果你想自己编译也可以用这个VLC iOS项目源码,核心部分还是在前面的那个git仓库里。
这里不得不说下,mobileVLCKit的API设计确实很棒,跟iOS的AVPlayer基本一样,替换起来非常方便,但是依然存在前面说的问题,而且他的速度比ffmpeg还要慢。为了一探究竟,我决定下载源码自己编译。
由于mobileVLCKit编译前,需要先编译libVLC,而libVLC没有提供debug版本,因此要断点调试应该是不可能,只能通过日志来了解他里面做了什么。通过这几行代码打开日志
VLCConsoleLogger *logger = [[VLCConsoleLogger alloc] init]; logger.level = kVLCLogLevelDebug; self.vlcPlayer.libraryInstance.loggers = @[logger];
以AWS上的一个dash视频为例,可以看到首先访问类似这样的链接
b-7660be6b.kinesisvideo.cn-north-1.amazonaws.com.cn/dash/v1/get…~
服务器返回了一个mpd文件,类似这样的格式
然后VLC解析里面的内容,解析完并初始化Template之后,就开始下载第一段Segment,也就是videoTrack, 从开始下载到解析完成大概要0.3s
而通过videoTrack的SegmentTemplate可知,一段track又分为好几次Sequence,每一个都有他的SequenceNumber,于是,VLC又去下载SequenceNumber为1和2的视频分段,每段从建立链接到下载再到解析完成大概耗时1s,至于为什么要下载两段,而不是一段,我想应该是跟他的缓存策略有关,因为1段只有2s,而我们的视频1s只有15帧,2段加起来才30帧,可能还没达到他的缓存下限,
从日志中可以看到,在下载完第2段视频后,他就开启了第二段Segment,也就是audioTrack的下载,
音频先下载了一段,然后下载第3段视频
然后再是第2、3段的音频,等三段音频都下载完成之后,才正式开始播放
同时他还会紧接着准备第4段视频和音频的下载
总的来说,LVC有一定的缓存策略,在开始阶段会下载更多的分段以保证后续播放的流畅性,但由于是单线程加载,加上缓存的数量多,因此出图也更慢。
方案四、ffmpeg与Cicada相结合
通过前面三个框架的对比测试和分析,我发现Cicada是最快的,同时我在网上搜了下播放dash的js库,例如dash.js,发现跟Cicada一样,也是没声音,因此怀疑是不是因为音频没解析出来的缘故。但不管怎样,Cicada里面对于dash的解析那部分是没问题的。而且从之前的日志来看,Cicada是音视频并行下载的,所以我决定把Cicada里面解析dash的部分移植到我们项目中作为Demuxer,然后再把Demux之后的packet给到ffpmeg进行解码并渲染。由于内容较多,就不写在这篇文章中了,这里只说下结果,就是既能支持G711,加载速度也跟单独使用Cicada是一样的,但是要花费的时间比较多(从迁移到适配现有工程大概前后花了一个星期多),最终我们采用了方案四,整体性能相对于最初的ffmpeg版本提升了50%多!
迁移Cicada(juejin.cn/post/740093…)