今天的新知系列课,我们邀请到了来自腾讯明眸·极速高清团队的技术导师 —— 赵军,为大家介绍FFmpeg以及媒体处理,并与大家就FFmpeg开发、开源与云的关系等问题做一些交流。本期课程也是本次新知系列课程的最后一期,希望各位都能从这一系列的课程中有所收获!也希望大家能够继续关注我们下一系列的课程。
业界的发展趋势及特点
首先我们来看一看业界的发展。第一,多媒体业务的流量目前在互联网是一个井喷式的爆发。思科报告预计,在2020年左右,亚太地区84%的互联网流量是Video。同时我们也知道关于多媒体的业务形态众多,典型的有短视频、社交、广电OTT、游戏竞技娱乐、直播、在线教育等等。
但是这个行业有它的一些特点,主要在于它的要求众多,然后也具备一定的门槛。以腾讯云音视频为例,我简单统计了一下音视频上面所用的Codec、容器格式和传输协议这些,目前至少有16个左右的Video或者Audio Codecs、11个容器格式、8个Streaming 的protocols、5类DRM,这还没有包括字幕这些。视频云的架构非常的有意思,初看上去并不特别的复杂,一般会分为发布、接入、媒体处理、mux 、DRM、播放以及分析统计等等。
但如果考虑到规模的话,你会发现它面临一些非常特殊的挑战,典型的有规模、可靠性、到达用户的可达性,然后整个音视频领域又特别的碎片化,还有,我们通常可能会忽略了运营成本,以及最终分发给客户的质量。
先用一句,我自己觉得是对音视频行业总括的话来说,这一句话来自于wm4,MPV播放器的作者,他说“multimedia is basically neverending pain——多媒体领域是一个永无止境的痛。“这可能是对我们这个行业最好的注释之一,不过在17年Demuxed上,Derek Buitenhuis加了后面的半句,他说”but we know, he still loves it。“
我们知道,虽然他在吐槽这个领域,但是他还一直喜欢这个行业。我想这就是我们做这个行业一些有意思的地方。今天我们的主题可能会涵盖4个部分,首先是FFmpeg的发展历程。然后是FFmpeg的框架,以及它在实际的媒体处理中该怎么使用。之后是云和开源之间的关系以及我们的一些思考。最后,我们也把碰到的一些open的question拿出来,和大家一起做一个简单的探讨。
FFmpeg的历史
FFmpeg创立于2000年。实际上,它最初是由Fabrice Bellard,作为MPlayer单独的解码库被创立出来,但随后被独立并开始逐步的支持更多容器格式,更多Codecs。Fabrice Bellard是一个不光在多媒体领域,也是整个IT行业的传奇人物之一,他的特点是创立一个项目后,差不多等到这个项目成型之后就会从这个项目离开,基本上不会再回来,所以Fabrice到了2003年,等FFmpeg这个项目已经初步成熟之后,就离开了项目,随后接手的核心人物是Michael。Michael作为这个项目的maintainer,始终是FFmpeg项目的灵魂人物。这中间除FFmpeg的发展历程以外,有一件事情我觉得很多人都可能有兴趣,在2011年的时候,FFmpeg的社区发生了一次变动,我们现在把它称之为“动乱”,原因是有一部分核心开发者认为FFmpeg社区出现了一些问题,他们创建了另外一个项目,称为Libav。
随后2011年到2013年,Michale开始从FFmpeg项目中回归。他主要做了两件事情,一个是快速的把Libav分离部分的功能rebase回来,另外一块是积极的去fix了安全相关的问题。在这之后,Libav社区又开始回归到FFmpeg社区,但这中间也导致了一些比较核心的开发者,至今仍脱离在FFmpeg社区之外。从2015年开始,这个社区又开始完全复苏。
FFmpeg它被定义为一个多平台的software project。它真正的组成大概有两块,第一块我们把它称为command line,典型的如 ffmpeg, ffplay, ffprobe, 还有已经被删除的ffserver。
第二部分则是它底层的一些c库,用于处理多媒体的各种事情,主要由c和汇编优化组成。另外它也是一个可扩充的框架,并且模块化和可配置性都做得非常的好,与此同时它也依赖了少量的外部项目,但是它的原则是除非必要才去依赖外部项目,目前它的外部项目主要聚集在External的第三方编码库,典型如 libx264 , libx265等等。
还需要强调一下的是,它是一个开源项目,使用它的时候需要遵循一定的准则,在使用的时候需要考虑清楚它的版权问题。另外,它的开发模式还是采用一些老派开源项目的方式,主要用git maillist,然后issue的跟踪使用trac。基于FFmpeg的源代码在开发之后,需要你把patches发送到对应的maillist去,然后由其他的maintainer或者contributor做对应的review,在得到ACK之后,这个patch才会被引入到主线。
另外你也可以通过patcwork(https://patchwork.ffmpeg.org/project/ffmpeg/list/)去看你的patch的状态。很多人之前在问FFmpeg的release流程大概怎么样。FFmpeg一般会在6个月左右做一次正式的release,与此同时它也有自己的回归测试项目被称为FATE(https://ffmpeg.org/fate.html),里面有大量的回归测试case,但是FATE也存在一些问题,主要的问题在于它对编码和Filter的支持并不是很好。
目前这个项目大概有30个左右的核心开发者,每年可能在2000~5000个提交左右。
了解了FFmpeg社区的情况,我们来看一看谁在用它。最初FFmpeg是被作为播放器项目而被引入到MPlaye(但是我们知道MPlayer现在已经进化成MPV);同时像VLC这种媒体中心/播放器也在使用它,媒体编辑工具基本上也没办法脱离FFmpeg的范围;FFmpeg对现在的Cloud Media也提供了大量帮助,典型如腾讯云以及微软Azure、 Facebook 、Yotube以及亚马逊的AWS都是FFmpeg的重度用户之一。还有一些不得不提的有意思的场景如浏览器等。另外,在2020年NASA的毅力号火星车登陆火星的时候,NASA技术人员回顾了两个毅力号火星车所使用的开源项目,其中之一是Linux kernel,另外一个就是FFmpeg。
FFmpeg——多媒体领域的瑞士军刀
对于FFmpeg有两个点需要大家记住,第一它是一个开源项目,第二。它是我们多媒体领域的瑞士军刀,这其实也意味着,这个项目的多样性以及它在行业的基础地位。
这个图里面,我们把FFmpeg一些基础组件放在里面,主要是avformat 、avcodec、avfilter,同时也会看到有外部 Codec合作引入,表明FFmpeg可能会对外部的一些库有依赖。
FFmpeg支持的格式非常众多,来细致的看一下FFmpeg组件。FFmpeg从架构上,分为两个部分。第一个部分我们把它称为命令行工具,主要是一些非常便利使用的工具,大部分人实际上都是从这些命令行开始尝试这个项目。其中ffmpeg 这个工具使用的最多,它是我们做转码的工具。另外的话是ffplay,它是一个非常简单的播放器,底层使用的SDL。第三个是ffprobe,它被用来从多媒体文件中萃取各种信息,另外还需要提及一下在历史上曾经存在过的ffserver,但后来因为维护的原因,已经被从FFmpeg项目中清除出去,现在,我个人可能会更建议用SRS这样的项目去替代ffserver,作为一个流媒体服务器。
它的底层是我们讲的这些libraries。libraries其实也可以分成两类,第一类是作为核心的多媒体库:
- libavformart,它解决了mux和demux的问题,支撑了MP4、MKV,、RTSP、RTP、RTMP等协议和容器格式。
- libavcodec,这个库实现的是Audio和Video的编码和解码。刚才也有提及,现在的编码大部分依赖第三方的库。
- libavfilter,这个库主要实现各种特效,对Video来说典型如缩放、去噪声、deinterlace等。
- 除去这些核心库,它的下层其实还有一些基础库,一个是libavdevice,是一些 Common tool library,还有一个libswresample,以及libpostproc、libswscale, 主要解决缩放和格式转换的问题。这一些基础库也有单独使用,但是可能没有像 libavformart、 libavcodec和libavfilter在行业有那么高的影响力。
紧接着我们看一个实际的例子,在FFmpeg里面我们怎样去做transcoding,怎样去做Player?
首先我在前面提及过FFmpeg支持了大量的IO输入方式,典型的有文件、网络流、设备,甚至于pipeline;读入文件后,它会进入到demuxer做一个splitter,把我们的Video和Audio进行分离。分离之后,原始流会分别进入decoder,decoder之后一般会进入到 filter, 对于Video来说会有scale,会有cropping之类操作, 对于Audio来说,一般会有一些下采样;filter之后进入encoding,Video和Audio各自进入encoding,随后会在avformat的地方作为mux保留下来。最常用的mux格式是我们常见的MP4、 MPEG-TS、FLV等等,这是一个transcoding的流程。下半部分是一个简化版本的播放器,它和上面比较大的区别在于没有encoder的流程,同时它也会在播放侧做一个Video和Audio的同步。
前面提及了FFmpeg pipeline。我们来看一个实际的例子,FFmpeg命令行实际上并不是特别好懂,需要自己去理一理。
ffmpeg -i smpte.mov -c:v libx264 -c:a aac smpte.mp4
上面是一个FFmpeg的命令行的例子,这里面其实等同为5个部分,第一个命令行表征了我们要去run FFmpeg的command,-i它实际上是告诉FFmpeg 命令行工具去把它作为一个input的source。第三个就非常明确了,它明显是我们的输入文件,-c:v它实际上说的是 output的时候使用这个codec去进行编码,在这里面我们对于Videos使用了libx264,对于Audio使用了aac,最终mux成MP4。使用FFmpeg的时候,有一些图,我觉得可以帮大家去理解demux以及转码之间的差异。
上面这个图,它实际上是一个转码,输入的Video部分编码采用h.264,Audio部分采用aac。经过转码之后,容器格式依然可以保证是MP4,但是Video采用了h.265的编码,Audio其实还是做了编码,但仍然是aac。
这一个你会看到它有一些差异,这就是典型的转复用,也被称为转封装,它的输出容器格式和输入是不一样的,编码格式和前面例子一样。通过转复用了之后,改变了容器格式,对于编码内容并没有做任何的更改,所以Video和Audio部分分别还是跟源一样,只是在mux的时候做了一些改变,这两者可能是FFmpeg最典型的应用场景之一。
也简单的提一下ffprobe。ffprobe它是一个分析工具,有一些很好玩的命令,如果大家对于容器格式碰到问题的话,可以去熟悉一下这个工具。它的命令行的格式形式和ffmpeg基本上是一致的,但是它也有一些自己特定的命令,比如说show_streams, show_format、show_packets。这样的话你能拿到各种信息去做对应的流分析,对我们诊断问题会带来大量的帮助。
下面的话我也给了两个link,如果大家有时间可以去看一看,他给了大量的例子。
https://ffmpeg.org/ffprobe.html
https://trac.ffmpeg.org/wiki/FFprobleTips
介绍完FFmpeg命令行,我们可以深入看一看FFmpeg的内幕,我觉得这个事情有点像看魔术师背后的箱子。先介绍一些比较基础的知识,看一看容器格式。很多人不太理解容器格式是怎么回事,我觉得这张图的话可能很形象的解释了容器格式,它看过来有点像我们用电线去包裹不同的导线,这些导线是我们的Video,是我们的Audio,是我们的Subtitles,是我们的data和txt的数据。容器格式就是把这些全部包裹在一起,做相应的传输、存储等等。在FFmpeg的实现中也有一些对应的概念。FFmpeg里面你会经常看到几个词,第一个是Codec和容器。第二个是流,这就有点像我们图中的这每一股导线,Video是一路流,Audio也是一路流,Subtitles也是一路流。以上面这个图为例,三条黄色的Vedio线你可以认为是有三路Video,有两条绿色的Audio线,你可以认为是有两条Audio。提及这个流其实需要提及这个流是怎么产生的,这就涉及到Codec的事情。
Codec的话,我们在后面可以再简单的解释一下,FFmpeg的底层还涉及到两个概念,第一个是Frame, 即帧,第二个是包,这两个概念初看有一些复杂,但是在后面我们解释一下之后,你会发现这两个概念并没有那么的复杂,理解起来也会非常的直观。
FFmpeg API的使用其实非常的简单。
从这个例子来看,你要使用FFmpeg只需要做4步,这4步有一点像我们把大象装进冰箱,第一步我们会打开一个文件。第二步我们会去读这个文件里面的流,从流里面去拿到对应的包,然后把这个包解为帧。第三步我们会continue到第二步轮询。第4步我们会把这个解出来的帧做一些对应的事情,典型如播放或者存储,如此往复。FFmpeg的API使用非常简单,大概看下来的话可能只需要20行左右就能完成一个播放的任务。
它所经历的流程实际上就在这个图里面,我们会通过一个文件把它解释成不同的流,这些流又会分成不同的packet,这些packet通过decode的解析变成了AVFrame。AVFrame就是没有压缩过的YUV数据。
接下来我们更细致的讲一下Codec容器以及传输格式相关的一些知识。首先我们看一个问题,在实际场景里面,是不是只用关心H.264或者H.265、aac以及MP4这个容器?在前面我就提及过腾讯云音视频涉及了大量的Codec。举例来说,前一代的像MPGE2、微软的VC1以及现在使用最为广泛的H.264,还有谷歌的VP8以及我们国产的AVS、AVS 等等,后面的话会有H.265、VP9、AVS2以及现在马上就要开始落地的AV1、AVS3、VVC、EVC、LCEVC等等。
Video有大量的格式出现,容器格式也是多种多样,常见的有MPEG2-TS/PS、MP4以及在国内盛行的FLV,还有开源格式MKV以及基于MKV改动的webM。传输协议其实也是多种多样,以上行、下行为例,首先我们上行部分有RTMP、WebRTC、SRT等。下行可能会有HTTP、FLV、TS、MP4还有安防领域的GB28181,下行的话还有一些ABR的协议,最典型的代表是HLS以及DASH。另外还有一些特定的传输协议在里面,像SRT以及现在逐步开始扩张的QUIC,还有WebRTC。刚才提及的传输DRM,也有不同的选择,所以你会看到这实际上是一个比较分裂的世界,怎么样在这一个分裂的世界里面去找到一个比较好的方式来解决问题,这对于多媒体领域是一个非常大的挑战。
我们更往前走进看一看,如果要处理多媒体的话,很多人首先会对Pixel format有一些疑惑,原因是它在内存存储的时候实际上可能会有一个packing的过程,你会看到在右侧实际上真正存储所需要的空间比实际需要的空间会更多。后面我们来看一看被很多人忽视的一个领域----颜色,看到我列出来的这些,大部分人都会倒吸一口气,因为光颜色在多媒体里面就会面临有Color family、Color primaries、Transfer function、Color matrix、Color range (tv or pc)。需要提一下Color range,这一块如果大家有兴趣可以去看一看,PC提供了一个0-255的全范畴范围,但是对于TV来说,它提供的实际上是一个受限的范围。
另外我们也知道比特深度在逐步的增加,目前使用最多的是8bit,但实际上10bit和12bit在不久的将来就会开始普及。分辨率也在逐步的增大,当前我们大部分看到的流可能在1080p或者720p,但是4k和8k的流即将来到。另外在处理媒体格式的时候,我们需要关注Interlaced、Field order、Pixel Aspect ration、Display Aspect ration。对于Audio来说,我们需要关注channel,chanel的布局,采样格式,采样率。多媒体领域还有两个非常有意思的问题,DTS、PTS。一般认为没有解决过DTS和PTS的人,可能没有完全入门多媒体领域。前面提及了FFmpeg里面有两个非常重要的概念,第一个是AVFrame,第二个是AVPacket。实际上因为AVFrame它存储的是原始的音视频数据,也有可能会包含字幕数据,简单来说它是一个没有被压缩的数据,具体而言你可以认为它处于解码后或者编码前。对于音频来说,大部分情况下它是一个所谓的PCM格式,对于视频来说它实际上是一个没有被压缩的一帧图像、一个WAV,对于字幕这是一个时间段内的一句或者几句文字。AVPacket它存储的是字节流数据,包含了被压缩过的音视频、字幕、附件信息等等,基本上你可以简单理解为压缩后的数据。它在FFmpeg中一般处于编码后或者解码前,然后封装的时候基本上封装都是AVPacket的,然后在解码的时候我们是先解出AVPacket,然后再到AVFrame。
提及了前面的这些基础知识,我们来近距离的看一看FFmpeg的总底层库。
首先我们要看的是Libavformat ,它提供的是demux和mux的功能,以我的经验来看,如果FFmpeg都不能支持的格式,非常的少,除非你是一个私有格式,要不然的话很难有存活之地。另外他也提供了各种协议的支持,这主要是为了做input的支持,典型如本地的文件, RTSP, HTTP, FTP, RTP, HLS, DASH等等。它的核心structures是AVFormatContext, AVOutputFormat, AVInputFormat还有前面提及到的 AVStream, URLProtocol。几者之间的层次关系如下图。
从FFmpeg内部来看,AVInputFormat实际上是demux的一个实现,比如你需要读取一个MP4文件,你其实是需要一个MP4容器解析对象来实现功能,这个对象会被存储在AVInputFormat里面。有demux就有对应的mux,但这一个格式被整体放在AVOutputFormat里面,如果你要写生成一个MP4的文件,就需要一个MP4容器封装对象来实现功能。FFmpeg的实现,实际上遵循了类和对象的一些基础的理念。
前面提及了AVInputFormat和AVOutputFormat,实际上真正使用的时候我们更多的使用AVFormatContext来处理,比如说读取读写MP4文件时。
前面有提及一个文件,它会按照Video、Audio、 Subtitle等被分割成不同的AVStream,每一个文件里面类型不同,AVStream的类型也不同。AVStream表现的是一个数据流,基本成员里面有codec,context,有这个流总时长,总帧数,帧率(fps数)/码率等等还有metadata 。AVFormat它是一个工作的核心,有源的路径,InputFormat作为输入,OutputFormat作为输出,所有的数据流,格式分类,时间参数,以及seek等等。
讲完Libavformat,我们可以看一看Libavcodec。Libavcodec提供的是一个Audio或者Video的decoder和encoder的能力。
目前FFmpeg支持的Video codec超过200个,Audio codec超过150个,还有基本上你能够看到的所有的Subtitle codec格式,它的核心结构体在AVCodecContext和AVCodec的这两者。
FFmpeg实际上提供了两类codec的封装方式,在前面我们有所提及。第一种我们把它称为- native open-source codec,最典型的例子是h264的decoder。对于encoder,很多是以第三方库的形式进入FFmpeg,比如libx264,libmp3lame ,libfdk_aac, libvpx等等,添加一个新codec到里面的过程非常的简单,基本上只需要注册上图的结构体。AVCodec和AVCodecContext的构成和前面的AVFormat非常类似,也是实现了一个类和一个对象的管理。
Codec核心接口,我觉得有必要再提及一次。原来的时候我们去使用Codec的接口,只需要使用类似于avcodec_decode_video2() and avcodec_decode_audio4()这样的接口,它是一个同步的接口。但是在后期FFmpeg社区进行了一次重构,这次重构把avcodec_send_packet()和avcodec_receive_frame()做了分离。重构的基本原因在于,对原始的API它保证的是一个packet的对应一定是一帧,但实际上对于MVC场景来说,一个packet有可能会对应两帧,甚至于更多,这意味着并不是一个一一对应的关系,再加上一些性能上的考虑,最后FFmpeg的社区,把 API构造成了两个。虽然现在FFmpeg社区已经把它分割成了两个分离的API,但实际上底层的改造并没有完全的完成,而只是部分的Codec支持完成。
第三个部分是Libavfilter,Libavfilter它提供的是一个Audio和Video的后处理,典型如scaling, de-interlace, FRC, denoise, audio resample等等,上图展示的是一个线性关系,但实际上avfilter可以构成一个有向无环图。avfilter一直处于重构状态,整体的性能并不算特别的好,使用它的时候需要更多的注意。
FFmpeg性能优化
多媒体领域还有一个非常重要的主题就是性能。FFmpeg作为最流行的多媒体基础库之一,最近这两年整个社区都在硬件加速方面做了大量的努力,使得FFmpeg也正在逐步的成为一个跨平台,跨OS,跨硬件厂商的通用硬件加速方案。
对于硬件来说,我们需要关注三个问题,第一个媒体处理,第二个通用计算,第三个显示与渲染。面临的挑战有哪些呢?不同OS上面的硬件加速API并不一致,windows上面用DXVA、DXVA2、Media Foundation,macOS和iOS上面用 VideoToolbox,Linux上面的话就更为分裂,有英特尔的VAAPI,Nvidia的VDPAU。社区同时也提供了两套接口,一个是V4L2,一个是M2M。安卓上面也有它自己的mediacodec,不同的硬件厂商也有自己的硬件加速方案,以GPU为例,AMD提供了AMF,Nvidia在编码上面提供了人NVECN/NVDEC,英特尔提供的mediaSDK正在逐步往OneAPI上面升级,而且有些厂商还有自己特定的方案。我还没有提及FPGA厂商的私有方案,除了OS和不同的硬件设备厂商,行业标准其实也是多种多样,典型有CUDA,还有想着跟CUDA对抗的OpenCL、OpenGL、Vulkan、Metal等等。
在谈及这个问题的时候,我觉得需要去考虑一下,对于FFmpeg来说,接口和框架到底该怎么样去做考量,这个事情有一点像我们自己去建房子,建完房子之后,我们需要有自己的小院子,但是面临一个问题,把什么放在院子里面,把什么放在院子外面,这是第一。第二,我们在什么地方开门。第三也很重要,有些人他并不喜欢你这个墙,他甚至想把墙推倒。在接口与框架上面,我觉得就和造院子一样,怎么样去考虑你这个项目的范畴。所以FFmpeg提了一个方案,可以去屏蔽不同的OS,不同的硬件平台,不同的Codec细节,同时也能构建一个比较灵活的media pipeline。
提到性能问题,我觉得管理大师德鲁克的一句话需要我们拿出来说一说。德鲁克说“If you can’t measure it, you can’t improve it。”是说如果你不能量化它,你就不能提升它,这是一个性能问题,要解决时候的基础。所有的性能问题你需要先有一个量化的数据,才能看到性能问题的提升,在多媒体领域,性能问题永远是一个痛点。
但是在做性能优化之前,Donald老师说“提前优化是万恶之源”。他的意思是说什么?是说除非你确认它是一个性能问题,否则不要启动优化。但是对于多媒体领域来说,经常会碰到这种问题,它差不多是我们这个行业永恒的话题之一。
前面也有提及优化的前提,你需要理解算法,需要去理解数据结构。第二你需要有数据说话,你需要有合适的Profiling。我们先看一看我们能利用的硬件都有哪些,对于CPU来说我们会能看到多核,我们看到多线程,我们能看到指令级别的SIMD,我们能看到更微观层次Cache,但是GPU该怎么做?FPGA还有 SoC。对于CPU来说有两大利器我们要好好地使用,第一线程,FFmpeg里面解码实际上提供了Frame级别的线程和Slice级别的线程,对于Filter来说只提供了Slice级别的线程,大家可以考虑一下为什么对于Filter来说并没有提供基于Frame的线程。
对于编码器来说,x264自身有Frame、Slice、SIMD级别的优化,另外SIMD它又解决了什么呢?SIMD简单理解的话,有一点像我们去切黄瓜。你把黄瓜想切成丁的时候,你可能是先把它切成条,然后再按照条,一条一条的去切,但有一个更简单但更高效的办法,切成条之后把所有的条并在一起,一刀下去,这实际上就是SIMD的优化。
GPU优化的方式一般来说会有CUDA、OpenCL,OpenCL在有些情况下面也可以用来做基于CPU的优化。我们知道Vulkan想尝试着来统一OpenCL和OpenGL,甚至于Open AL,但是它能统一吗?现在看来可能还是个未知数,原因是微软现在去做DXVA2、苹果则是去做了Metal,这样的话我们看到加速接口的标准仍然是多方并没统一的标准。
线程加速的核心其实是释放多核的能力。FFmpeg跟Filter里面有一个典型的Flag,表明该filter做了一个Slice的线程加速,它做了什么?它实际上提供了一个工作组的模式,把不相关的数据以行或者列为单位划分,同时批量处理提升性能。我们在内部有个项目通过线程简单的加速之后,处理时间从7毫秒降低到2毫秒,提升速度达3.5倍。
FFmpeg的SIMD加速。我们先看一看SIMD加速方式会有哪一些,一般来讲汇编加速会有三种模式,第一种是intrinsics,他的做法有一点像用C函数代码去写汇编语言,所有的汇编指令都被封装成c函数。第二种内联汇编,他的问题在于他和编译器强关联,导致没办法去换平台。第三种手写汇编。
FFmpeg目前选择的是第三种方式,原因是当年的Intrinsics在不同的编译器之间会引入性能损耗。FFmpeg所以就使用了手写的汇编,另外它借用了大量的x264的汇编优化函数。
但性能优化其实需要从最简单,但收益最大的地方开始,经常会碰到一个窘境,有的同事会过来问我性能不太好,我会提示他,让他去看一看,你把你所有的CPU 核心用起来了吗?
从实际情况去看,你会发现像这个图一样,其中的1个核心在干活,其他的8个核心在围观,这样的话你即使让这一个人他的负载100%,平摊到8个核心之后,总体的负载大概也只有12.5%,所以我们第一要务其实要把多核使用起来,看一看这些真实的故事。
我们在一个特定的领域里面,在一个特定的场景里面增加了一行代码,一条复杂的链路执行时间减半,它是什么样的背景?它实际上是我们在使用FFmpeg的时候,在一个服务器的场景并没有去更细致的控制线程的力度,导致解码并没有充分使用多核多线程,导致解码时间非常的缓慢,在最后我们调整解码线程相关的设定,解码时间立即减少,整条链路的时间减半。
第二个故事跟它也有关系,过了两天我同事过来问我说“嘿哥们我有一个进程,现在拥有可能超过1200的线程”,这个事让我当时有点大吃一惊,后来发现在使用FFmpeg的时候,他的Avfilter实际上会根据CPU的核心数创建线程,但是很不幸,这位同事他使用了多个Avfilter的graph实例,造成的情况实际上是等于一个N * 核心数的线程数,它中间用了大量的Filter,然后我们是一个48核心的机器,最终导致了1200 个线程在一个进程。
第三个故事我觉得这个事情也挺有意思,这个故事实际上是跟谷歌有一些关系。QA总是告诉我,在他的测试环境环有个crash问题,但很不幸,在我的环境里面始终不能复现这个问题,后来发现我和他使用的CPU有所差异,它使用一个服务器96核的CPU,由此可见,多线程的问题,即使像谷歌这样的公司,它也未必考虑的那么周全。
另外,线程它只是影响性能吗?在我们这个领域它还有一个特定的影响,还会影响编码的质量。这个图清晰的展示了随着线程的增加、图像质量的降低而达到一个阈值之后,它有可能会再度上升,所以怎么样选择一个合适的线程模型,对于多媒体处理绝对是一个有挑战的事。
SIMD优化它也会面临一些挑战
- 发现数据变形其实并非易事,所有的事情并不像切黄瓜那么的简单,事情跟事情之间数据和数据之间可能会存在一些依赖,这种复杂性有的时候会很难被揭露。
- 不同的硬件之间的一致性会面临不同。举个例子来说,x86和arm他们在SIMD的支持上面就会不一致,但是我们可能同时需要支持x86需要支持arm,这样你怎么样保证你的优化在不同的平台都能够做支持,这是一个很大的挑战。
- 边界情况的处理,大部分的SIMD都有内存对齐的需求,该怎么样去处理。
- 算法里面SIMD需要尽量避免分支,但实际上逻辑处理可能是我们编程中最常见的行为之一,怎么样去避免这样的分支,对很多算法来说也是一个挑战。
腾讯明眸背后的媒体处理
以FFmpeg作为一个实际的例子,最初到腾讯云音视频的时候,整个部门大概拥有38个FFmpeg的repo,你会看到各式各样的轮子,三角形、半圆形……这其实也反映了一个问题,在公司内部,不管是腾讯这样的大公司,还是我们的友商,还是另外的小公司,怎么样去跟社区把关系理清晰,怎么样去统一FFmpeg都是一个挑战。
我们在FFmpeg上做的这些事,又该怎么去反馈到社区呢?这个我觉得,特别是对国内的开发者来说,是需要考量的事情。实际上不同的Feature,甚至于Bug Fix、你的性能优化、文档更新、甚至于是一个小sample,这对社区都是一个良性的贡献。
我们在腾讯云音视频打造了一个和社区双向互动,正向的工作流程。我们在内部维护了FFmpeg的一个稳定版本,逐步的把公司内部的不同版本合并到这个版本统一维护,同时我们也把在工作中发现的一些问题或者优化快速的反馈给社区,以达到和社区版本逐步趋同的地步。
我们也对开源与云的关系做了一个思考。我们知道软件正在吞噬这个世界, Marc Andreessen曾经说过这样的话,在此过程中我们也做了一些思考,第一,开源技术它降低了技术研发的门槛,但是它并不是一个真正的产品,它并没有解决产品开发需要考虑的性能、特性、运营等问题。第二,云厂商在使用开源项目的同时,其实应该避免是开源项目“吸血者”类似的指责,我们需要保持一个开放的态度和社区共生,积极的回馈开源社区。
我们来看看FFmpeg这个项目的使用。先从一个例子说起,上图是一个典型的监控场景。视频流通过摄像头解码,后期可能会经过Scale, CSC转换,随后绿色部分会进行一个对象探测,可能会把我们感兴趣的物体探测出来,探测出来之后,会跟踪它的轨迹,会做一个Object Tracking。在上面的部分实际上是将这个对象单独拿出来,对这个物体做一个分类。这是一个非常实际的场景,比如你在安防产品里面,需要把一个人从视频流中寻找出来,然后去追寻它在不同时间段的运行轨迹,最终把这个结果存储到服务器。在里面的话,你会看到绿色部分FFmpeg并不能涵盖,这其实也引入了一个问题。FFmpeg是瑞士军刀,那它能用来伐树吗?可能可以,但并不是一个最优的选择。
在此基础上面我们提出了自己的媒体处理框架——TMPF。以极速高清为例,它解决了线程优化调度,在极速高清中提供了不同的线程池,默认使用了一个分时的调度策略,其线程调度解决FFmpeg的内置的线程调度问题。另外在高分辨率情况下,提升了编码线程的优先级,减少编码带来的全路径延迟等待。最终的结果我们可以看到这一点比较小的优化为编码带来了10%左右的收益。
第二个为了应对于直播场景,我们在整个框架中实现了自定义sei帧透传,这样会对业务方带来很大的帮助,因为他不需要去理解整个sei,极速高清在2021年的MSU Cloud Video Transcoding Services Comparison中取得了非常好的成绩,获得了最佳的速度/质量第一名,获得了最佳的成本/质量的第一名。但是在易用性方面,TMPF现在也还有一些工作需在做。
一些开放性问题的讨论
提及了FFmpeg的开放性,FFmpeg在社区以及以及云和FFmpeg的一些关系,我觉得有一些开放的问题需要拿出来做一些讨论。
首先我们知道AI是现在的一个大热话题,但是AI框架是不是FFmpeg的一部分,我觉得这个问题有一点像哈姆雷特里面的一句原文,他说To be, or not to be, that is the question,是还是不是?始终是一个问题。如果要做,我们可能面临着这样的一个选择。第一,实现一个特定 inference filters?还是集成一个重度的inference框架?这实际上是一个非常困难的选择。
目前FFmpeg的状态是,它已经有一个简单版本的DNN的inference API,底层之间支持了部分dealing的功能,主要是Super Resolution,但性能堪忧。这个事情在我个人看来有一点像我们去做软件架构,我们期望架构像垒墙一般层次分明,只需要靠简单的粘合就能够把层次做清楚。但实际上当我们想把AI框架放到FFmpeg的时候,以opencv为例,它有一点像五花肉肥瘦相间,但也没办法去把肥肉和瘦肉清晰的分割。
举一个实际的例子,OpenCV在他自己的内部调用了FFmpeg的去做encoder和decoder,与此同时FFmpeg的filter的部分,调用OpenCV实现了部分的功能,所以它们两者之间的层次到底是什么?我觉得这有点像右边的五花肉根本无法去做更好的分割。
另外在我个人看来,AI框架太重,他并不是FFmpeg这个项目的范畴,任何一个项目它有自己的领域,什么事做什么事不做,相较而言,我认为说No,什么时候不做,比什么时候做可能会更为艰难,对FFmpeg这个项目来说,我个人认为AI框架不应当作为它的一个部分。
还有一些其他open question:
- FFmpeg的命令行在处理ABR的时候,转码是能堪忧,这一个问题在Twitch的博客中有详细阐述。
- 在大部分的项目中,我们是真的一定要使用FFmpeg的吗?这个问题我觉得也可以做一些更多的思考。
- FFmpeg API其实缺乏一些灵活度,简单以HLS来说,HLS它是一个典型的demux嵌套demux的场景,在连续HSL demux 中嵌套的HTTP部分,同时也嵌套了MPEG TS demuexer,如果HTTP的链接出现了相关问题,HLS demux如何感知到,这是一个问题。
- FFmpeg在版本升级的时候,剔除了以前的动态协议注册、Codec表注册、文字格式注册,这样导致所有的代码只能和FFmpeg融为一体,实际问题是什么?所有的部署,如果你需要先增加一个协议Codec或者容器格式,需要做一次重新编译,这在增量部署的时候可能会带来一些问题,因为原来我们可以通过一个动态库注册FFmpeg的增加部分,但是现在已经不大可能。
还有一个未来的问题, FFmpeg社区对于新的Codec的支持,目前也面临一些挑战。举例来说AV1 decoder、VVC decoder,目前都不是内置在里面,都不是原生编解码器。这对于FFmpeg的社区是一个非常大的挑战,后续怎么解决,还得看社区的这些人有什么样的智慧。
腾讯云音视频在音视频领域已有超过21年的技术积累,持续支持国内90%的音视频客户实现云上创新,独家具备 RT-ONE™ 全球网络,在此基础上,构建了业界最完整的 PaaS 产品家族,并通过腾讯云视立方 RT-Cube™ 提供All in One 的终端SDK,助力客户一键获取众多腾讯云音视频能力。腾讯云音视频为全真互联时代,提供坚实的数字化助力。