最近在做移动端音视频编解码,首先要实现的是移动端视频的解码功能。纯的 FFmpeg 方法在移动端也能实现,但是效率上的确要慢一些,1080p 的视频还好,但是上到 2k、4k,那个解码速度(以肉眼可见的速度解码一帧),就没法忍受了。因此要搞移动端硬件解码,以加速解码速度,同时释放部分 CPU 资源。
参考 FFmpeg 源码中 examples
参考 FFmpeg 官方源码中的 examples 的相关功能实现,来实现自己的程序设计,应该是最快的思路。但是,关于视频解码,FFmpeg 官方源码中,有 decode_video.c
demuxing_decoding.c
hw_decode.c
,这三个解码相关的文件。
其实前两个文件的方案差不多,只不过第一个针对裸 h264 流,而第二个是针对带封装的视频文件。建议新手可以参考第二个方案。
至于第三个文件,hw_decode.c
,看起来是一个硬件解码的 demo,当然,它的确也是(笑),然而,这里,我们却不能参考这个。参考这个文件,我们可以实现在 Linux 或者 Windows 平台上,利用 cuvid 或者 NVIDIA 、Intel 等硬件厂商实现的硬解码功能,实现硬件解码。但是,在 Android 平台使用 MediaCodec 的解码,却没有实现。
在尝试参考 hw_decode.c
实现 MediaCodec 硬解码的过程中,在 195 行 avcodec_get_hw_config
这一步失败,没有任何 config 列表可供选择。
因此,我们还是参考 demuxing_decoding.c
来实现 Android 平台 MediaCodec 硬解码。
Then,Why?MediaCodec 架构简析
因为 Android 是个平台,其硬件厂商多种多样,而 MediaCodec 并非是一个硬件厂商,因此它并不提供硬件编解码方案。实际上,MediaCodec 更像是一个中间层,通过 openMAX 继承硬件厂商的硬件编解码能力,最终,硬件厂商通过提供符合 openMAX 规范的硬件编解码库。因此,如果仿照 hw_encode.c
来实现,必然会在 avcodec_get_hw_config
这一层找不到合适的配置。
一处改动
那么,我们就完全可以参考 demuxing_decoding.c
来实现 Android 平台 MediaCodec 硬解码功能。其实,基本上全文拷贝到 Android native 代码中,即可使用,只需要改动一处。即 165 行的 dec = avcodec_find_decoder(st->codecpar->codec_id);
改为 dec = avcodec_find_decoder_by_name("h264_mediacodec");
即可。
为了方便,我们只解码文件中的视频流,同时简化整个流程,基本上,完整代码如下:
static AVFrame *decode_frame = nullptr;
int testFFmpegMediaCodec(bool sw) {
string filename = "/sdcard/pav/hd.mp4";
AVFormatContext *fmt_ctx_ = nullptr;
int ret = 0;
if (ret = avformat_open_input(&fmt_ctx_, filename.c_str(), nullptr, nullptr) < 0) {
LOGE("Failed open file %s, ret = %d", filename.c_str(), ret);
return -1;
}
if (avformat_find_stream_info(fmt_ctx_, nullptr) < 0) {
LOGE("Failed to find stream information.");
return -1;
}
int vst_idx = av_find_best_stream(fmt_ctx_, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, false);
if (vst_idx < 0) {
LOGE("Failed to find video stream from file %s", fmt_ctx_->filename);
return -1;
}
AVCodec *pCodec = avcodec_find_decoder_by_name("h264_mediacodec");
AVCodecContext* codec_context = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(codec_context, fmt_ctx_->streams[vst_idx]->codecpar);
if (ret = avcodec_open2(codec_context, pCodec, nullptr)) {
LOGE("Failed to open avcodec. ret = %d", ret);
return -1;
}
decode_frame = av_frame_alloc();
if (!decode_frame) {
LOGE("Failed to allocate frame.");
return -1;
}
decode_frame->format = codec_context->pix_fmt;
decode_frame->width = codec_context->width;
decode_frame->height = codec_context->height;
av_frame_get_buffer(decode_frame, 0);
ret = av_image_alloc(video_dst_data, video_dst_linesize, decode_frame->width, decode_frame->height, (AVPixelFormat)decode_frame->format, 1);
if (ret < 0) {
LOGE("Could not allocate raw video buffer.");
return -1;
} else {
LOGD("We allocate %d for raw video buffer.", ret);
}
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;
while (av_read_frame(fmt_ctx_, &pkt) >= 0) {
if (pkt.stream_index == vst_idx) {
ret = avcodec_send_packet(codec_context, pkt);
if (ret < 0) {
LOGE("Error submitting a packet for decoding (%s)", av_err2str(ret));
continue;
}
while (ret >= 0) {
ret = avcodec_receive_frame(codec_context, decode_frame);
if (ret < 0) {
if (ret == AVERROR_EOF) return 0;
if (ret == AVERROR(EAGAIN)) break;
}
// process with decode_frame
av_frame_unref(decode_frame);
break;
}
}
av_packet_unref(&pkt);
}
return 0;
}
一个大坑
以上代码仅为示例,不保证能直接运行,可能需要做些微小的调整。然而,即使以上代码没有问题,这个 Android MediaCodec 硬解码功能仍然不能实现。这其中,有个大坑。
当我把代码调整好运行起来之后,发现,程序在解码完第一帧之后,就一直报错:Error submitting a packet for decoding,EAGAIN。这个问题困扰了我很久,直到有网友提到说,需要先把缓冲区里的已解码的帧数据取出来,然后才能再次送入数据进行解码。
最简单快捷的改法,就是把 avcodec_send_packet
后面对 ret < 0
时的 continue
去掉,去掉之后,果然能持续解码了。但是会丢掉好多帧,因为当 ret < 0
时,读取到的 packet 并未成功送入解码队列中去。
因此,这里需要修改一下整个解码逻辑。修改为,先去试图取数据,取不到了,再读数据送入解码器队列。即修改为如下:
while (true) {
ret = avcodec_receive_frame(codec_context, decode_frame);
if (ret == 0) {
// process with decode_frame
av_frame_unref(decode_frame);
continue;
} else if (ret == AVERROR(EAGAIN)) {
ret = av_read_frame(fmt_ctx_, &pkt);
if (ret == AVERROR_EOF) return 0;
if (pkt.stream_index != vst_idx) {
av_packet_unref(&pkt); // 注意这一句,缺失将造成内存泄漏
continue;
}
ret = avcodec_send_packet(codec_context, pkt);
if (ret < 0) {
LOGE("Error submitting a packet for decoding (%s)", av_err2str(ret));
continue;
}
}
}
仅为示例代码。具体细节请自行处理。
结语
至此,利用 FFmpeg + MediaCodec 实现的 C++ 层 Android 硬解码功能就能正常实现了。
我在 小米 MIX 2S 手机上做了下简单的测试,1080 p 的 h264 视频解码完成能达到 25 倍速(即 25 秒钟的视频解码完成需要 1 秒钟),而 4k 的 h264 视频能达到 3.5 倍速。
然而,4k 的 h264 视频解码时有较大的概率出现花屏现象。其花屏的概率、程度,可能与手机的性能、状态均有关系。第一次调通硬解码时解了下 4k 的一段视频,发现花屏严重。而第二天,刚开始工作时解了下同一段视频,发现前面若干帧基本没有花屏现象,大约从三百帧开始,出现小的花屏,最后几帧花屏严重。
对于花屏这个问题,有网友称移动平台解码 4k 视频的确很吃力,很费劲,甚至解不动。如果这样的话,那么为什么那段 4k 的视频能在 小米 MIX 2S 手机上流畅播放呢?是不是还有什么参数可以继续调整优化呢?希望有高手能给出一些提示或建议。