关于FFmpeg(进阶)

基本篇在此

目前仅使用到了ffmpeg的读写文件/编解码功能,并未使用其去直接渲染。

这里说是进阶,其实也只能算浮于表面。因此主要记录使用ffmpeg以来遇到的坑,版本差异,android版本编译等。So,这里就不继续采用Q&A的方式了。

(本文并非通过ffmpeg.exe\bin传递command来进行处理,而是直接调用ffmpeg相关libraries中的api)

####FFmpeg libraries FFmpeg有以下库:(2014/12/24 ffmpeg 2.5.2),主要功能列在About中有基本介绍,这里不再赘述。

####FFmpeg基本 首先,官方文档为第一参考文档;其次就是源码(这里把源码放到这么靠前的原因下文会提到)。

FFmpeg的官方文档算是比较全的。 一些常见case的testbed都可以找得到;例如2.5版本,编解码的sample

同时官方的例子并非浅显易懂,但是因为层次简单,所以调用逻辑很清楚;参考着这些文档/Sample就可以按需设计一个简化版Adapter了。

####FFmpeg基本调用逻辑 还记得Decode/Encode和Muxer/Demuxer吧,如果还记得那就很好理解了。下面截选了部分代码(不要在意细节),有一些重要关键但非流程相关的步骤这里先省略(例如FIFO等),下文再提。


#####Encoding(Based on 2.x)

av_register_all();
avcodec_register_all();
/* 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);
res = avio_open(&m_p_fmt_ctx->pb, m_p_file_path, AVIO_FLAG_WRITE);
...
res = avformat_write_header(m_p_fmt_ctx, NULL);
...
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);
av_write_trailer(m_p_fmt_ctx);
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)

av_register_all();
avcodec_register_all();
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);
}
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);
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;处理流程为:

  1. audio buffer A在通过Resample后会先被转换为长度为8192的16位buffer B;

  2. write buffer B到FIFO中

  3. 从FIFO中read出长度为4096的buffer C

  4. Encode buffer C

  5. 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开源风险,副产品是清晰了框架。试试看吧。