V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  KSClive  ›  全部回复第 1 页 / 共 1 页
回复总数  3
2018-01-05 16:17:32 +08:00
回复了 KSClive 创建的主题 推广 FFmpeg 从入门到出家
FFmpeg 简析

FFmpeg 从无到有,发展至今,功能日益强大,代码也越来越多,很多初学者都被其众多的源文件、庞大的结构体和复杂的算法打消了继续学习的念头。本章节将从总体对 FFmpeg 进行简单的解析,教您如何阅读 FFmpeg 源码。

2.1 总体说明

FFmpeg 包含如下类库:

libavformat - 用于各种音视频封装格式的生成和解析,包括获取解码所需信息、读取音视频数据等功能。各种流媒体协议代码(如 rtmpproto.c 等)以及音视频格式的(解)复用代码(如 flvdec.c、flvenc.c 等)都位于该目录下。

libavcodec - 音视频各种格式的编解码。各种格式的编解码代码(如 aacenc.c、aacdec.c 等)都位于该目录下。

libavutil - 包含一些公共的工具函数的使用库,包括算数运算,字符操作等。

libswscale - 提供原始视频的比例缩放、色彩映射转换、图像颜色空间或格式转换的功能。

libswresample - 提供音频重采样,采样格式转换和混合等功能。

libavfilter - 各种音视频滤波器。

libpostproc - 用于后期效果处理,如图像的去块效应等。

libavdevice - 用于硬件的音视频采集、加速和显示。

如果您之前没有阅读 FFmpeg 代码的经验,建议优先阅读 libavformat、libavcodec 以及 libavutil 下面的代码,它们提供了音视频开发的最基本功能,应用范围也是最广的。

2.2 常用结构

FFmpeg 里面最常用的数据结构,按功能可大致分为以下几类(以下代码行数,以 branch: origin/release/3.4 为准):

1. 封装格式

◦AVFormatContext - 描述了媒体文件的构成及基本信息,是统领全局的基本结构体,贯穿程序始终,很多函数都要用它作为参数;

◦AVInputFormat - 解复用器对象,每种作为输入的封装格式(例如 FLV、MP4、TS 等)对应一个该结构体,如 libavformat/flvdec.c 的 ff_flv_demuxer ;

◦AVOutputFormat - 复用器对象,每种作为输出的封装格式(例如 FLV, MP4、TS 等)对应一个该结构体,如 libavformat/flvenc.c 的 ff_flv_muxer ;

◦AVStream - 用于描述一个视频 /音频流的相关数据信息。

2.编解码

◦AVCodecContext - 描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息;

◦AVCodec - 编解码器对象,每种编解码格式(例如 H.264 、AAC 等)对应一个该结构体,如 libavcodec/aacdec.c 的 ff_aac_decoder。每个 AVCodecContext 中含有一个 AVCodec ;

◦AVCodecParameters - 编解码参数,每个 AVStream 中都含有一个 AVCodecParameters,用来存放当前流的编解码参数。

3. 网络协议

◦AVIOContext - 管理输入输出数据的结构体;

◦URLProtocol - 描述了音视频数据传输所使用的协议,每种传输协议(例如 HTTP、RTMP)等,都会对应一个 URLProtocol 结构,如 libavformat/http.c 中的 ff_http_protocol ;

◦URLContext - 封装了协议对象及协议操作对象。

[if !supportLists]4. [endif]数据存放

◦AVPacket - 存放编码后、解码前的压缩数据,即 ES 数据;

◦AVFrame - 存放编码前、解码后的原始数据,如 YUV 格式的视频数据或 PCM 格式的音频数据等;

上述结构体的关系图如下所示(箭头表示派生出):

图 2. FFmpeg 结构体关系图
2.3 代码结构

下面这段代码完成了读取媒体文件中音视频数据的基本功能,本节以此为例,分析 FFmpeg 内部代码的调用逻辑。

char *url = "http://192.168.1.105/test.flv";

AVPacket pkt;

int ret = 0;

//注册复用器、编码器等

av_register_all();

avformat_network_init();

//打开文件

AVFormatContext *fmtCtx = avformat_alloc_context();

ret = avformat_open_input(&fmtCtx, url, NULL, NULL);

ret = avformat_find_stream_info(fmtCtx, NULL);

//读取音视频数据

while(ret >= 0)

{

ret = av_read_frame(s, &pkt);

}

2.3.1 注册

av_register_all 函数的作用是注册一系列的(解)复用器、编 /解码器等。它在所有基于 FFmpeg 的应用程序中几乎都是第一个被调用的,只有调用了该函数,才能使用复用器、编码器等。

static void register_all(void)

{

avcodec_register_all();



/* (de)muxers */

……

REGISTER_MUXDEMUX(FLV, flv);

……

}

REGISTER_MUXDEMUX 实际上调用的是 av_register_input_format 和 av_register_output_format,通过这两个方法,将(解)复用器分别添加到了全局变量 first_iformat 与 first_oformat 链表的最后位置。

编 /解码其注册过程相同,此处不再赘述。

2.3.2 文件打开

FFmpeg 读取媒体数据的过程始于 avformat_open_input,该方法中完成了媒体文件的打开和格式探测的功能。但 FFmpeg 是如何找到正确的流媒体协议和解复用器呢?可以看到 avformat_open_input 方法中调用了 init_input 函数,在这里面完成了查找流媒体协议和解复用器的工作。

static intinit_input(AVFormatContext *s, const char *filename,

AVDictionary **options)

{

int ret;

……

if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)

return ret;



if (s->iformat)

return 0;

return av_probe_input_buffer2(s->pb, &s->iformat, filename,

s, 0, s->format_probesize);

}

[if !supportLists]1. [endif]s->io_open 实际上调用的就是 io_open_default,它最终调用到 url_find_protocol 方法。

static conststructURLProtocol *url_find_protocol(const char *filename)

{

constURLProtocol **protocols;

……

protocols = ffurl_get_protocols(NULL, NULL);

if (!protocols)

return NULL;

for (i = 0; protocols[i]; i++) {

constURLProtocol *up = protocols[i];

if (!strcmp(proto_str, up->name)) {

av_freep(&protocols);

return up;

}

if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME &&

!strcmp(proto_nested, up->name)) {

av_freep(&protocols);

return up;

}

}

av_freep(&protocols);



return NULL;

}

ffurl_get_protocols 可以得到当前编译的 FFmpeg 支持的所有流媒体协议,通过 url 的 scheme 和 protocol->name 相比较,得到正确的 protocol。例如本例中 URLProtocol 最终指向了 libavformat/http.c 中的 ff_http_protocol。

[if !supportLists]1. [endif]av_probe_input_buffer2 最终调用到 av_probe_input_format3,该方法遍历所有的解复用器,即 first_iformat 链表中的所有节点,调用它们的 read_probe()函数计算匹配得分,函数最终返回计算找到的最匹配的解复用器。本例中 AVInputFormat 最终指向了 libavformat/flvdec.c 中的 ff_flv_demuxer。

2.3.3 数据读取

av_read_frame 作用是读取媒体数据中的每个音视频帧,该方法中最关键的地方就是调用了 AVInputFormat 的 read_packet()方法。AVInputFormat 的 read_packet()是一个函数指针,指向当前的 AVInputFormat 的读取数据的函数。在本例中,AVInputFormat 为 ff_flv_demuxer,也就是说 read_packet 最终指向了 flv_read_packet。
2017-12-18 16:23:57 +08:00
回复了 KSClive 创建的主题 云计算 Android 短视频 SDK 转码实践
一. 前言

一些涉及的基本概念:

转码:一般指多媒体文件格式的转换,比如分辨率、码率、封装格式等;
解复用(demux):从某种封装中分离出视频 track 和音频 track,然后交给后续模块进行处理;
复用(mux):将视频压缩数据(例如 H.264 )和音频压缩数据(例如 AAC )合并到某种封装格式的文件中去。常提到的 MP4 即是一种封装;
编码(encode):通过专门的算法(例如 H.264 或 AAC)来对原始音视频数据进行压缩;
解码(decode):对压缩后的数据进行解压缩。
短视频 APP 中录制完成后,为什么要做转码:

原始视频文件码率较大,上传下载都需要很长时间,不利于传播;
编辑时增加特效、转场效果后,只是在预览中有效,原始文件并未改变,需要进行一次转码来把这些效果合成进最终的文件;
多段视频进行编辑前转码拼接为一个文件,方便后续的编辑;
目标格式和源文件格式不一致,比如需要从 mp4 转成 gif。

为什么不在服务端做转码呢?

短视频需要加入滤镜等效果,在移动端转码可以充分利用手机的 GPU 等资源,实现实时添加滤镜实时看到效果;
原始视频码率较大,上传下载都需要很长时间。

其中 Audio Filter 和 Video Filter 分别是指音频和视频的预处理。

短视频转码的时机:
多段视频的导入;
转场完的合成;
编辑完的合成。

二. Demuxer 方案的选择

Demuxer 模块的实现,主要有以下三种方案:

方案一,使用播放器
播放器的主要功能是播放,也就是从原始文件 /流中提取出音视频,按照 pts 完成音视频的渲染。转码并不需要渲染,要求在保持音视频同步的情况下,尽快把解码数据重新按要求编码成新的音视频包,重新复用成文件。我们也曾经为了实现尽快这个要求,把播放器强行改造成快速播放的模式,但后来遇到了很多问题:

音视频同步时机的问题,视频的解码是慢于音频的解码,必然需要实现同步逻辑。player 中如果改成快速播放模式,player 内部加上音视频同步的逻辑,改动非常大。如果 player 不管同步,解码数据直接上抛给调用层,则需要在短视频上层做音视频同步,引入了额外的工作量;
使用硬解码时,从 SurfaceTexture 中获取的 timestamp 不准。因此最后放弃了这个方案。
方案二,使用 MediaExtractor
MediaExtractor 是 Android 系统封装好的用来分离容器中的视频 track 和音频 track 的 Java 类。优点是使用简单,缺点是支持的格式有限。

方案三,使用 FFmpeg
使用 FFmpeg 的 av_read_frame API 来做解复用,即实现简易版的播放器逻辑。

优点:FFmpeg 中对视频格式有大量兼容的逻辑,相比 MediaExtractor 兼容性好,增加新的输入格式的支持会更容易,同时音视频同步逻辑的控制更简单;
缺点: 需要引用 FFmpeg,相对来说 SDK 体积较大。
方案二的兼容性不如方案三。相比方案一,方案三把音视频的解复用和解码都放到了同一个线程,av_read_frame 能输出同步交织的音视频 packet,上层逻辑调用更清晰。
同时短视频其他功能模块已经引入了 FFmpeg,转码模块引入 FFmpeg 并不增加包大小,所以选择了 FFmpeg 方案。

三. 转码的数据传递

金山云多媒体 SDK 实践中,Demuxer 实际上是在 C 层做的,但是接口的封装是在 Java 层。解码结构也是一样。Demuxer 和 Decoder 之间如何高效地在 Java 和 C 层之间传递待解码的音视频包?

3.1 AVPacket 的传递

FFmpeg 的 demuxer 模块解复用出来的为音频或视频的 AVPacket。最开始的时候我们并没有在 Java 层对整个 AVPacket 的地址指针进行封装,而是把数据封装在 ByteBuffer 和其他的参数中。这样遇到了很多因为 AVPacket 中的参数没有传递到解码模块导致的问题。

最终我们通过 intptr_t 在 C 层保存 AVPacket 的指针,同时在 Java 层以 long 类型来保存和传递这个指针,解决了这个问题。

3.2 AVFormatContext/AVCodecParams 的传递

为了实现模块的复用,我们把 Demuxer 和 Decoder 分成了两个模块。使用 FFmpeg 来实现时,Decoder 模块可以和 Demuxer 模块共用 AVFormatContext,通过 AVFormatContext 来创建 AVCodecContext。

但是这样会有一个问题,Demuxer 的工作速度会快于 Decoder,此时 AVFormatContext 是由 Demuxer 来创建的,Demuxer 停止的时候会释放 AVFormatContext。如果交给 Decoder 模块来释放,不利于模块的复用和解耦。最终我们发现在 FFmpeg 3.3 的版本中,AVCodecParams 结构图中有 Decoder 所需要的全部信息,可以通过传递 AVCodecParams 来构造 AVCodecContext。

四. 转码提速

转码的速度是客户非常关心的一个点,转码时间太长,用户体验会非常差。我们花了非常多的精力来对短视频的转码时间进行提速。经验主要有以下这些点:

4.1 调整视频软编编码参数

转码的时间大部分都被视频的编码占用了,我们把 x264 编码做了调整,在保证画质影响较小的前提下,节省了 30%以上的编码时间。

4.2 优化 GPU 数据读取

使用视频软编时,如何从 GPU 中把数据“下载”到 CPU 上,我们尝试了很多中方案,具体的我们会在另一篇文章中详细解释。之前的方案是使用 ImageReader 读取 RGBA 数据。优化为用 OpenGL ES 将 RGBA 转换为 YUVA。读取数据后从 YUVA 再转为 I420,下载和格式转化总耗时,提速了大约 40%。

4.3 开启硬编

硬编的缺点: 在 Android 平台上,硬编的兼容性较差,同时视频硬编的压缩比差于软编。
硬编的优点是显而易见的,编码器速度快,占用的资源也相对较少。

4.4 开启硬解

经过大量的测试,硬解的兼容性相较于硬编会好很多,使用硬解码,直接使用 MediaCodec 渲染到 texture 上,省去手动上传 YUV 的步骤,也节省了软解码的时间开销。

4.4.1 硬编解遇到的坑

关于 Android 的硬编解网上已经有很多例子,官方文档也比较完善。不过在实现过程中还是会遇到一些意想不到的问题。

图像质量的问题
在硬编上线后,我们对比画质发现转码图像质量较差。原因是使用 MediaCodec API 时,选择的是 MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR,CBR 的好处是码率比较稳定,但是会牺牲画质,移动直播中选用 CBR 更合理。短视频转码场景硬编时推荐使用 MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR,VBR 会获得更好的图像质量。对于软编时,我们也尝试过 ABR(也就是 VBR),但实际测试下来效果并不能保证。

硬解不兼容 AVCC/HVCC 码流格式
H.264 码流主要分 Annex-B 和 AVCC 两种格式,H.265 码流主要分为 Annex-B 和 HVCC 格式。AnnexB 与 AVCC/HVCC 的区别在于参数集与帧格式,AnnexB 的参数集 sps、pps 以 NAL 的形式存在码流中(带内传输),以 startcode 分割 NAL。
而 AVCC/HVCC 的参数集存储在 extradata 中(带外传输),使用 NALU 长度(固定字节,通常为 4 字节,从 extradata 中解析)分隔 NAL,通常 MP4、MKV 使用 AVCC 格式来存储。
Android 的硬解只接受 Annex-B 格式的码流,所以在解码 MP4 Demux 出的视频流时,需要解析 extradata,取出 sps、pps,通过 CSD(Codec-Specific Data)来初始化解码器;并且将 AVCC 码流转换为 Annex-B,在 ffmpeg 中使用 h264_mp4toannexb_filter 或 hevc_mp4toannexb 做转换。

硬解时间戳不准确的问题
硬解码器解码视频到 Surface,此时通过 SurfaceTexture.getTimestamp()获得时间戳并不准确,某些机型会出现异常。所以还是要使用解码输入的时间戳,可将解码过程由异步转为同步,或者将 pts 存储到队列中来实现。

音频硬编硬解解的速度
MediaCodec 的音频编解码具体实现和机型有关,许多机型的 MediaCodec 音频编解码工作仍然是软件方案。经过测试 MediaCodec 音频硬编码较软编码有 6%左右的提速,但 MediaCodec 音频硬解反而比软解的的速度慢,具体原因有待进一步调查。不过这只是部分机型的测试结果,更多机型的比较大家可以使用我们 demo 的转码 /合成功能进行测试。

4.5 转码提速对比

下面以三星 S8 为例,短视频 SDK 在转码速度上的进步,更多机型的对比数据,请移步 github wiki 查看。

将 1 分钟 1080p 18Mbps 视频,转码成 540p 1.2Mbps,不同版本时间开销大致如下:

机型 版本 编码方式 第一次合成时长 第二次合成时长 第三次合成时长 平均值
三星 S8 V1.0.4 软编 52s 54s 58s 54.7s
V1.1.2 软编 49s 50s 50s 49.7s
V1.1.2 硬编 35s 36s 38s 36.3s
V1.4.7 硬编 21.5s 21.9s 22.5s 22.0s
可以看到,使用了硬编、硬解等提速手段后,合成速度由 54 秒优化到 22 秒。

五. 模块化的思考

金山云短视频 SDK 的基础模块是基于直播 SDK,整体来说,是一套 push 模式的流水线。
流水线中的每个模块都很好地实现了解耦,单独模块完成单一的功能,模块的复用也非常方便。前置模块在产生新的音视频帧后,会立即 push 给后续模块,后续模块需要尽快把前置模块产生的音视频帧消化掉,最大程度上保证实时性。为了保证音视频同步等逻辑,引入了大量同步锁。在短视频的开发中,遇到了不少的死锁和不方便。对于短视频这种非实时的场景,更多的时候,需要由后续模块(而非前置模块)来控制整个流程的进度。
当前处理过程中需要实现暂停,需要在前置模块加锁来实现。为了能方便以后的开发,我们会在接下来重新梳理这种 push 流水线的方式, 实现模块化的同时,尽量减少同步锁的使用。

六. 总结

转码对于普通用户来说不可见的,但却是短视频 SDK 的一个重要过程。怎么样让转码过程耗时更短,转码图像质量更高,特效添加更灵活,减少我们团队自身的开发和维护成本,同时也为开发者提供最方便易用的 API,一直是金山云多媒体 SDK 团队的目标。
团队在很用心的开发短视频 SDK,欢迎试用!

转载请注明:
作者金山视频云,首发简书 Jianshu.com

Android 短视频 SDK: https://github.com/ksvc/KSYMediaEditorKit_Android
有关音视频的更多精彩内容,请参考 https://github.com/ksvc

金山云 SDK 相关的 QQ 交流群:

视频云技术交流群:574179720
视频云 Android 技术交流:620036233

作者:金山视频云
链接: http://www.jianshu.com/p/bfb9ee91572a
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2808 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 12ms · UTC 15:05 · PVG 23:05 · LAX 07:05 · JFK 10:05
Developed with CodeLauncher
♥ Do have faith in what you're doing.