关于FFmpeg(进阶)
Posted
基本篇在此。
目前仅使用到了ffmpeg的读写文件/编解码功能,并未使用其去直接渲染。
这里说是进阶,其实也只能算浮于表面。因此主要记录使用ffmpeg以来遇到的坑,版本差异,android版本编译等。So,这里就不继续采用Q&A的方式了。
(本文并非通过ffmpeg.exe\bin传递command来进行处理,而是直接调用ffmpeg相关libraries中的api)
####FFmpeg libraries FFmpeg有以下库:(2014/12/24 ffmpeg 2.5.2),主要功能列在About中有基本介绍,这里不再赘述。
- libavutil 54. 15.100
- libavcodec 56. 13.100
- libavformat 56. 15.102
- libavdevice 56. 3.100 — 未使用
- libavfilter 5. 2.103 — 未使用
- libavresample 2. 1. 0 — 未使用
- libswscale 3. 1.101
- libswresample 1. 1.100
- libpostproc 53. 3.100 — 未使用
####FFmpeg基本 首先,官方文档为第一参考文档;其次就是源码(这里把源码放到这么靠前的原因下文会提到)。
FFmpeg的官方文档算是比较全的。 一些常见case的testbed都可以找得到;例如2.5版本,编解码的sample
同时官方的例子并非浅显易懂,但是因为层次简单,所以调用逻辑很清楚;参考着这些文档/Sample就可以按需设计一个简化版Adapter了。
####FFmpeg基本调用逻辑 还记得Decode/Encode和Muxer/Demuxer吧,如果还记得那就很好理解了。下面截选了部分代码(不要在意细节),有一些重要关键但非流程相关的步骤这里先省略(例如FIFO等),下文再提。
#####Encoding(Based on 2.x)
- Register all codec //应该是plugin的机制
av_register_all();
avcodec_register_all();
- Find encoder -> create stream -> open encoder
/* find the audio encoder */
p_audio_codec = avcodec_find_encoder(m_p_fmt_ctx->oformat->audio_codec);
if (!p_audio_codec) {
res = ErrorNoEncoderFound;
goto EXIT;
}
// Try create stream.
m_p_audio_stream = avformat_new_stream(m_p_fmt_ctx, p_audio_codec);
if (!m_p_audio_stream) {
LOGE("Cannot add new audio stream\n");
res = ErrorNoStreamFound;
goto EXIT;
}
p_codec_ctx = m_p_audio_stream->codec;
... //some regular configurations on encoder
// open the codec.
res = avcodec_open2(p_codec_ctx, p_audio_codec, &p_options);
CHK_RES(res);
- Open File -> write header
res = avio_open(&m_p_fmt_ctx->pb, m_p_file_path, AVIO_FLAG_WRITE);
...
res = avformat_write_header(m_p_fmt_ctx, NULL);
...
- Encode
av_init_packet(&output_packet);
...
res = avcodec_encode_audio2(output_audio_stream->codec, &output_packet, frame, data_present);
...
res = av_interleaved_write_frame(output_format_context, &output_packet);
- Write trailer
av_write_trailer(m_p_fmt_ctx);
- Release
avcodec_close(m_p_audio_stream->codec);
avio_close(m_p_fmt_ctx->pb);
avformat_free_context(m_p_fmt_ctx);
#####Decoding(Based on 2.x)
- Register all codec
av_register_all();
avcodec_register_all();
- Open File -> find stream -> find decoder -> open decoder
res = avformat_open_input(&m_p_fmt_ctx, filename, NULL, NULL);
if (res < 0) {
LOGE("Could not open find stream info (error '%s')\n",
get_error_text(res));
goto EXIT;
}
/** Get information on the input file (number of streams etc.). */
res = avformat_find_stream_info(m_p_fmt_ctx, NULL);
if (res < 0) {
LOGE("Could not open find stream info (error '%s')\n", get_error_text(res));
goto EXIT;
}
...//match stream via stream->codec->codec_type
if (m_p_video_stream) {
// Find the decoder for the video stream
p_video_codec = avcodec_find_decoder(p_video_ctx->codec_id);
if (!p_video_codec) {
LOGE("avcodec_find_decoder() error: Unsupported video format or codec not found: %d. ", p_video_ctx->codec_id);
}
// Open video codec
if (p_video_codec && (res = avcodec_open2(p_video_ctx, p_video_codec, NULL)) < 0) {
LOGE("avcodec_open2() error %d: Could not open video codec.", res);
}
- Decode //考虑到视频编码的特殊性(前后帧参考),所以这里一般是需要do-while的。
av_init_packet(&pkt);
res = av_read_frame(m_p_fmt_ctx, &pkt);
if (res < 0) {
av_free_packet(&pkt);
break;
}
...
// Decode frame
decoded_length = avcodec_decode_video2(m_p_video_stream->codec, temp_frame, &got_frame, &pkt);
av_free_packet(&pkt);
- Release
avcodec_close(m_p_audio_stream->codec);
avformat_close_input(&m_p_fmt_ctx);
####Video/Audio格式转换(对齐) 不管是audio还是video,编码前/解码后都需要做格式转换,来保持数据格式的一致性。不然拿RGBA的数据直接送给等待NV12的encoder也不太好吧~
Video做格式转换是通过sws_scale
(scale);需要注意的是,decoder/encoder需要对数据做一次对齐(为了XX位对齐做的buffer填充),换言之,即使是640 x 640的视频,decode出来的数据也不可以直接用,而需要scale一次来消除“渐隐区”(文档貌似没写,翻源码吧亲)。
Audio做格式转换是通过swr_convert
(resample),用法类似sws_scale
。
但audio有一点不同在于,video是一帧一帧的,而audio,就是一段连续数据而已。所以一帧video frame在scale以后还是一帧video frame;但一段长度为A(表示对应时长为Ta)的audio buffer在resample之后很可能就变成了长度为B(表示对应时长依然为Ta)的audio buffer。
举个简单例子,其他格式都一样,格式为8位(AV_SAMPLE_FMT_S8
)的audio buffer,转换为16位(AV_SAMPLE_FMT_S16
)后buffer长度会翻倍。这个“转换公式”很好理解吧,当然格式差异很多的时候,这个转换公式也跟着会复杂的多。(当然如果无视buffer格式差异,某些case下也是可以正常encode/decode的,但声音就不对了~)
长度的不同,导致了在decode/encode时需要一个额外的机制,来配合“对齐”(这里用对齐似乎更合适吧)buffer。
也就有了下文的FIFO(2.x版本后提供了av_audio_fifo_xxx
的一系列接口,在这之前,audio接口调用逻辑和video接口差别很多,FIFO直接被整合在audio接口中而并未独立出来;听起来有点像是为了框架清晰而增加调用复杂度的故事 ^_^)
####Audio FIFO 在编码前/解码后,audio buffer为了保持格式(这里的格式包括采样率等)上的一致性,需要先通过FIFO进行一次buffer的重整。
即:
解码:Decode -> FIFO -> Resample-> audio buffer expected
编码:audio buffer expected -> Resample -> FIFO -> Encode
举个例子,假设一段待Encode的audio buffer格式为8位,长度(framesize)为4096;encoder期望的格式是16位,长度为4096;处理流程为:
-
audio buffer A在通过Resample后会先被转换为长度为8192的16位buffer B;
-
write buffer B到FIFO中
-
从FIFO中read出长度为4096的buffer C
-
Encode buffer C
-
3-4循环
调用起来和一般的fifo队列基本一致,EOF的时候注意对剩余buffer进行处理(一般是少的部分填空白)就好了。
####内存泄露 (版本2.4.3)对,就是内存泄露。更准确的说,是一些ffmpeg内部api的“特殊”逻辑,导致了内存泄露。
还记得前面提到的,用sws_scale来处理的“渐隐区”么? 如果存在渐隐区,就意味着,每次ffmpeg都需要一块额外的buffer,来作为实际encode之前的一个缓冲区。
比较好的方案是一开始create一个temporary buffer,最后释放掉;差一点的方案,即使每次都申请,那么至少每次call av_free_packet
的时候也应该释放掉;当然我们每次都“臆测”ffmpeg已经做了至少差一点的方案吧。
但FFmpeg采用的方案是。。。不 去 释 放 。。。(好吧,很难说这就是ffmpeg的issue,也可以被解释成调用逻辑不合理T_T)
所以如果发现app(想象一下:android工程,jni调用挂起so,用Monkeyrunner跑压力测试,通过排除法来看哪里出了内存泄露。。。也是醉了)有OOM,不妨往这个方向试试看。
(目前不确定最新的(2014/12/24,版本2.5.2)是否还存在这个情况,待补充)
####版本差异(尤其是老版本与2.x的差异) 研究不多,个人面对的最大的差异体现在audio部分(其他部分都是参数数量/类型的细微调整,无关调用逻辑);
为了保持audio/video的decode/encode接口基本一致(洁癖嘛?),FIFO被剥离,导致audio调用逻辑相比较之前要复杂一些;
好处嘛,首先代码看起来舒服多了,其次就是可以“跟”上ffmpeg平均两周一更的步伐(这算好事吧?)
####Android版本编译 直接参考Roman的文章即可。 NDK R10类似。
当然也可以直接用这个ffmpeg4android@github(也是我在维护)
####FFmpegAdapter 源码上传到FFmpegAdapter@github,草草上传(疏漏甚多,以后会尽量保持更新)。
最简单的用法就是直接inherit,这样load-library,jni接口映射(这里使用的是ndk默认映射机制,不是manual那套),error-code等都不需要单独写,至少我是这么直接用的。
主要目的是规避GPL开源风险,副产品是清晰了框架。试试看吧。