ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-07-29 13:03:23  阅读:867  来源: 互联网

标签:FFmpeg 解码 avcodec ret decode 有个 av MediaCodec frame


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

标签:FFmpeg,解码,avcodec,ret,decode,有个,av,MediaCodec,frame
来源: https://blog.csdn.net/u014248312/article/details/119192574

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有