“师兄”带你看FFmpeg开发中的坑起坑落

2021-09-02 11:57:31 浏览数 (1)

对于FFmpeg每一个从事音视频开发的小伙伴都不会感到陌生,它可以说涵盖了音视频开发中绝大多数的领域,不过在FFmpeg开发中也会遇到可移植性、转码压缩音视频不同步、多线程编解码等等问题,本文是“大师兄”刘歧在LiveVideoStack Meet北京站上的分享,他将与大家分享FFmpeg实践开发中遇到的技术难点和经验。

演讲 / 刘歧

整理 / LiveVideoStack

谢谢大家,谢谢主持人,因为今天时间有限,所以就简单的介绍一些套路。先做下自我介绍,我是一个音视频流媒体的爱好者,目前和几个朋友一起成立了公司,专门做音视频编解码处理,当然不是做编码器,是专门做在线处理。此外我是FFmpeg的维护者之一,再就是以前玩过嵌入式处理,是从44B0开始的;也做过存储,参与开发过广电的大规模存储;在中科创达专门做手机时做过设备驱动开发;也做过一些流媒体,当时主要基于高通平台;之后去蓝讯之后开始做流媒体系统设计,当时担任流媒体架构师,主要是做直播部分。

在正式开始前,跟大家分享一组图片,第一个是现实世界色彩风格的我,第二个是泰坦尼克色彩风格的我,第三个是斯巴达克斯色彩风格的我,三种不同的风格,这个是我前两天调色彩风格时专门生成的一套模板,所有的视频处理完成之后,你会得到各种各样的效果。

我今天要分享的主题前面也有提到,整体内容大概会分为五部分,首先是基础介绍,然后会大致分享做FFmpeg开发的规则,开发过程中遇到的坑,以及面对这些坑如何分析问题,最后我会做一个简单的总结。

基础介绍

  • 基本概念

什么是压缩,很多人都会遇到过客户要求把MP4转码成FLV,但如果里边是H.264的话是不需要去做转码的,只需要做remux,也就是转封装即可,那封装是什么?我们用一个很生动的例子来说明——液化气罐,它就是一个封装,它里面装的是咱们压缩的东西,可以理解为里面装的是H.264,如果我把开关打开之后,里面的水将会变成气体,这个气体可以理解为咱们看到的YUV或者PCM,这些数据量是很大的,然后做一些压缩,把它装到一个封装里面,这个封装就是液化气罐,我们把液化气罐砸了就是个解封装的过程。

在现实生活中,我们看一些视频或者是做一些编解码处理的时候会遇到一些问题,比如说看到这张图片,这个是从封装里面解耦气体的时候出现了异常,但这个异常又不是特别严重,那么出现的现象就是右边花屏的状态,这样的情况顶多是丢了一帧,还没有导致严重的错误。

如果再稍微过分一点,它就会导致成上图显示的现象——播放器基本已经没法用了,你需要有两种处理方式,当然不可能是拿水往电脑上泼,那么这张图上的处理方式就是播放器自动帮你处理——退出、崩溃。这里不得不提到FFmpeg中fatal error跟error两者的区别,我们前面看到的第一张图就是一个error,也就是说虽然出错了,但是还能继续去处理下一个包;但如果要是遇到了fatal error,比如说没有idr帧,它的帧整个都是乱序的,或者是播放器的Buffer已经溢出了,或者是干脆无法解码,这个时候它就是一个fatal error,播放器就会退出来了。

  • FFmpeg常用功能&软件

大概介绍一下FFmpeg,其实FFmpeg中大家常用的功能主要是libavformat、libavcodec和libavfilter,当然还有一些包括采样率的转换、缩放、格式的转换,比如YUV转成RGB,以及最近非常火的人工智能,做一些头像识别等等,那么做头像识别之前,我们知道人眼看到的内容是有色彩的,如果想去识别它,可以把它转成一个RGB格式,那么我们再看到这个图像的时候,它将会是头发是黑的,脸有可能是白的,那么它就可以去做识别了,这部分实际可以通过滤镜部分去处理,此外转换的时候通过scale做处理。再就是AVUtil,我个人认为它是非常厉害的功能,它里面会抛出来一些函数,那些函数和接口或者一些条件判断、一些流程控制,都是通过util抛出来的,在FFmpeg官方文档中有util的目录。还有libavdevice,在做一些AVFoundtion,或者是qtkit、xorg、dshow、alsa等等一些设备在里面是可以拿到。

接下来介绍下FFmpeg四个比较常用的软件,第一个是ffmpeg,大家做转码常用的,或者是测一些流是否可以被正常播放,或者在有异常的时候,通过它input下看是否能正常获取到。第二个是ffprobe,它的功能很强大,通过它可以分析到关于音视频各个维度的的数据,比如我拿到一个音频包,这个包多大,头部存了什么东西?如果主播推流,中间被其他人抢占,再重新推流这个过程中,客户端首先会收到metadata,然后是Sequence Header,紧接着就是视频或者音频的流,主播重新推流之后就会出现多一个metadata的情况,那这些用ffprobe是可以看到的。或者时间戳是否连续、时间戳跳变等等也可以通过ffprobe可以看到。ffprobe可以read packets、 read frames、read streams,如果你想看整个帧的排列,可以分析它的frames,通过导出CSV格式文件转换成图像——柱状图。

第三个是ffplay,实际上很多时候可以用ffplay直接去播放,可以立即看到效果,不必再等待FFmpeg转码。需要注意的是ffplay在2015年中旬,从SDL1.2转成SDL2.0,如果出现编不出ffplay的情况,可能需要装一下SDL2.0。最后是ffserver,虽然目前也还会有很多人使用,但确实已经没落了。

FFmpeg开发规则

接下来介绍下FFmpeg开发的基本规则,其实这个规则很简单,但是不符合大部分人的使用习惯。

  • 会读文档是成功的第一步

FFmpeg会提供一个很全面的文档,因此首先你得会看文档,文档的结构很清晰,首先第一步就是advanced options,下面是各个封装部分和codec部分,最后是滤镜部分。

第二个是Wiki,这个文档都是通过texi文件去写的,我们可以看到document下面有muxer和codec的.texi文件,在增加或者是删除一个选项的时候,这个文件是必须要改的,否则代码是无法被合并的,然而它只是纯文字的,有些抽象的东西无法举例的时候,就会写一个Wiki放到track里面。此外就是API使用文档。

  • 套路满满的玩

除了这些基本规则外,还要掌握一些套路,比如说看代码看不懂的时候怎么办?邮件列表是个很好的手段,它是全球完全公开的,通过它可以帮助你了解这段代码是如何实现的,或者它有怎样的问题等等。这里跟大家分享一个问题。

这段代码是用HLS的duration做了一个四舍五入,HLS的duration分成两部分:一个是最上边的header部分taget duration,还有就是最下边的INF里边要设置duration。标准文档规定,在做round的四舍五入时, taget具体切的duration,也就是每个切片实际的duration必须小于taget duration,但是在官方文档中写的比较晦涩。这个问题最终通过邮件列表的方式得以解决,所以善用邮件列表可以帮助大家解决很多问题。

  • 入坑也是要按“基本法”的

了解完文档和基本套路,接下来就是入坑了,我的建议还是要仔细阅读文档,这是最简单的,也是最直接的,它包括了常见的FAQ、支持的封装格式、编码格式、滤镜以及外部库。再就是开发者说明文档,在提交代码之前先要阅读开发者说明文档,它会告诉我们怎么去开发,怎么去生成Patch,怎么去发一个邮件。此外还有Git的基本使用,比如通过Git cherry-pick命令可以把另一个开源社区代码的commit ID直接合并到你branch的commit ID上,避免在move过程中丢失author信息,但也会有冲突,就需要自己手动fix,它相当于是一个项目管理,你可以通过git去管理多个branch代码。最后是自动化测试环境,如果你想提代码,比如想给HLS加一个选项,在生成HLS的时候,m3u8,最后会加一个endlist,而下次再去写这个文件时,因为FFmpeg没有追加的功能,它会从头重新生成一遍,这样就会导致像CDN中常见的一个场景——主播推流断了之后重新推上来,因为原来的流被覆盖掉了,所以一部分手机播放会出现卡顿,如果增加这个功能,它实际上是把endlist去掉,加了一个discontinuity的tag。

那年那坑那些经验

  • 踩坑实录第一弹

接下来跟大家分享下过往遇到的坑和一些经验,首先是CDN,做CDN服务不能针对某一个业务做优化,而是需要针对所有的业务,这里给大家举个例子:有的客户会要求直接基于FLV去追加,但是假如用户在推流过程中出现抖动,就会导致断开连接,就需要重新推流上来,而很多情况下会要求算成一次推流,那么就需要去解决拼接的问题。

第二个是时常变化的标准,比如像DASH、MPEGTS或者HLS,前段时间MPEGTS增加了一个音频的Codec——OPUS,而HLS最近也终于定版——RFC。

第三点是文档信息,这部分经常会出现文档信息不全,像OpenCV很多文档是基于代码直接生成的,也有手动去写的,之前我在解决OpenCV人脸识别问题的时候,发现原来是函数接口名变了,但文档中没有变更,而FFmpeg在这方面还是不错的,它在你提代码的时候会强制你写文档。

最后是想要的功能不支持,比如facedetect,就需要自己把它加进去,这时首先需要研究里面有哪些东西、大概是做什么的,比如人脸识别是放在AVFilter的滤镜中,因此其中会有对应的参考;再比如缩放,缩放是很耗资源的,在FFmpeg中把缩放计算这部分做了硬件加速,包括VAAPI、QSV、NVIDIA和Intel对应的计算,都是通过AVFilter去做的。

刚才提到不支持人脸识别功能,就需要分析VF overlay,这个.C文件里面,我们可以看到它处理了RGB、 YUV以及gray,其中是各种不同的叠加,那么它既然能这样处理,说明在这个滤镜里面肯定有Input和Output,那么我就可以直接把它的Input数据放到OpenCV里面,再把OpenCV中处理过的数据,也就是Output数据放到Outputlabel里面就可以了,实际上它的整个操作是很简单的,而且除了Overlay这个滤镜,其他的滤镜处理也很简单,代码都不会超过500行。

  • 踩坑实录第二弹

第二个坑就是FFmpeg不支持DASH Demuxer,像我们平时看的YouTube、BBC、CNN,它们的直播流有两种——没有RTMP和FLV,其中一个是HLS,另一个是DASH,包括我们看到的Netflix中很多直播也都是支持的。现在的FFmpeg也已经可以支持,虽然暂时还不支持多码率。第二点是对HLS Muxer的支持,HLS在以前仅仅可以做到大概的解,在封装成m3u8的时候,其中很多功能都不支持,比如single_file或者切多片时的设置,在切多片的时候就需要用到类似single_file的功能,比如做到三百兆一个片,这样就可以避免HLS都是零零碎碎的小片,对硬盘做到一定的保护以及提升一些硬盘读取的速度,而且IO消耗并没有那么高。最后是FLV,我们在以前做FFmpeg封装的时候,推流出去后,由CDN给录制成FLV,它的metadata里是没有keyframe index的,如果用Flash播放器去播放或者拖动的时候是很慢的,因为它需要下载这个数据,但是假如有这个信息,就能够做到很快的定位,因为所有的keyframe信息都在keyframe index中记录,而它在metadata里,我只需要解一下metadata就可以把它拿出来。

分析问题的基本套路

其实学习和使用FFmpeg是需要一个基本套路的,我认为这个套路就是复现问题。当遇到一个问题时,首先复现这个问题,从中找到它的规则。然后有针对性的去看参考文档,如果参考标准里面并没有完全的说明,那就只能去支持它的这种不标准;如果在参考标准中明确提出,就需要做更改,因为代码不能被合并。第三件事是分析,分析正确的与不正确之间的差别,这里举个例子,我在做HLS支持的时候,HLS 我将FMP4支持上去后发现最开始无法播放,我就跟苹果的FMP4作对比,分析两者的区别,发现是yuvj422p的pixfmt,然后按照常规的yuv420p做编码压缩在封装fmp4,就可以了,就是通过这样的对比找到最终问题所在,这个也是当初做服务大概的思路。

那么遇到问题应该怎么办?作为程序员经常会遇到这样一种情况。

产品经理或者客服直接找到你,说客户过来报bug了,说你的代码有问题,必须马上解决。

这种时候我特别想给他一枪,把他解决了,问题也就解决了,相信很多人也会跟我有一样的想法(哈哈)。但现实却是,经过一番苦战之后,“是在下输了”。

  • 复现问题——DASH篇

这时候就需要开始分析这些问题、复现问题,首先需要有一本标准文档,针对DASH支持这个具体问题上,我们还需要找一个播放器测试,以及了解FFmpeg的框架,在实际往FFmpeg里加DASH功能时,第一步我先分析它是一个切片文件列表,然后我开始去分析HLS,因为它们之间有很多相似之处,区别在于一个是纯文本,一个是xml,此外还需要注意标准中描述的一些细节,比如DASH实际上不仅仅通过CX,逐个递增的切片,还有一种timeline的方式,需要按照它的时间去做文件的获取,但它不会写明切片的连接是什么,只会告诉你计算方式,如果算错就会导致拿不到文件,这个时候就需要特别注意:比如我的第一片first_seq_no,获取当前时间get_current_time,获得availability_start_time以及time_shift_buffer_depth,还包括time的计算方式fragment_timescale等等。

  • 复现问题——HLS篇

再跟大家分享另一个案例——HLS,图片中生成的这个切片,如果不仔细观察可能看不出有问题,实际上这个视频少写了一帧,大家可以看到生成的最后一片和拿到的最后一片,它们duration时间是不同的,所以我分析是,它是在append的时候直接就退出了,并没有写最后一帧。

  • 套路的基本点

接下来介绍下FFmpeg中写Demuxer需要注意的几个基本点:首先read_probe, 注意它不能直接返回0,这个是需要注意的,需要去访问Maxscore命令的size才可以,或者max size score除以2,当然这是正确的情况,如果不正确的可以直接返回0,那么解析将会结束;接着就是解析header,这个header就是FLV的头,或者DASH的xml文件;然后是read_packet,实际它在调用API的时候更加常用,对于写封装而言用到它的机会并不会很多,它是在IO操作中,使用read_packet调里面的回调,再就是read_close和read_seek。

write部分其实主要是三部分——write_header、write_packet和write_trailer,这里需要注意一点,很多人在写MP4的时候经常会Ctrl C,这个时候你剩下的MP4就播不了,因为两次Ctrl C的时候是不写trailer的,写一半被中断了,而moov box是根据你的MDat记录的所有信息计算出来的,然后通过一个flag,把moov box移到最前面。

最后再介绍下IO操作部分,当你read_packet的时候,实际上它会调AVFormatContext,其中的pb就是具体封装的context,你可以去通过IO接口设置,把它挂到AVFormatContext的pb里,此后所有的操作其实就都是操作这个pb,也就是说read_packet的时候是在read里面的回调——类似于read_data,拿到read_data就可以正常解析音视频数据。

其实总体而言,套路的基本点就是静下心来多看代码,读文档,写代码,除了这三件大事更重要的就是交流,因为独学而无友则孤陋而寡闻。以上是我分享的内容,感谢大家。

关于分享者

刘歧,OnVideo 联合创始人、ChinaUnix资深版主、FFmpeg Maintainer/顾问,擅长流媒体系统设计,分布式系统开发。

广告时间

12月2日,『后直播时代技术』沙龙将走进成都,LiveVideoStack携手腾讯音视频实验室、声网、又拍云等知名企业一同直击游戏、社交领域,探索其在多媒体与音视频技术的应用实践。

0 人点赞