【AVD】FFmpeg + MediaCodec 实现 Android 硬件解码,中间有个大坑

最近在做移动端音视频编解码,首先要实现的是移动端视频的解码功能。纯的 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 手机上流畅播放呢?是不是还有什么参数可以继续调整优化呢?希望有高手能给出一些提示或建议。

上一篇:MediaCodec对超分的支持


下一篇:【六】Android MediaPlayer整体架构源码分析 -【start请求播放处理流程】【Part 4】【01】