在前面的博文中咱们介绍了关于运用NDK编译FFMpeg6.0的一些坑以及相关的解决方法。

详情请参考:NDK编译ffmpeg6.0与x264的坑

在写《NDK编译ffmpeg6.0与x264的坑》一文的时分就说过了,咱们编译FFmpeg6.0的意图便是为了体验一下它NDK式的MediaCodec硬解码以及硬编码。

今日咱们就在android上运用FFmpeg6.0来体验一下它的硬解码,经过FFmpeg调用MediaCodec将视频数据解码为yuv数据并保存。

关于FFmpeg在android上硬解码的相关博文之前已经写过一篇博文:

ffmpeg之硬解码

仅仅之前的需求经过注册JNI的方法调用MediaCodec,但这在FFmpeg6.0之后不需求了。

在这儿趁便提一下一个关于学习ffmpeg的方法,众所周知,其实最好的学习材料便是官方的材料,没有比官方更权威的材料了。 一般在ffmpeg的源码目录doc/examples下就有很多比如,例如咱们想学习下ffmpeg硬解码的比如,就能够研究该目录下的hw_decode.c这个比如。

FFmpeg调用MediaCodec解码

FFmpeg6.0运用MediaCodec硬解码

下面说说运用FFmpeg调用MediaCodec进行硬解码的介个过程:

  1. 翻开编译选项

首要,要让FFmpeg支撑MediaCodec硬解码,在交叉编译时就要翻开相关装备,主要是enable一些与MediaCodec相关的属性:

--enable-hwaccels \
--enable-jni \
--enable-mediacodec \
--enable-decoder=h264_mediacodec \
--enable-decoder=hevc_mediacodec \
--enable-decoder=mpeg4_mediacodec \
--enable-hwaccel=h264_mediacodec \
  1. 找到对应的解码器

一般情况下假如咱们不重视软解码仍是硬解码的话经过解码器ID运用avcodec_find_decoder函数获取到对应的解码器即可。 可是假如咱们想要运用硬解码,一般会运用函数avcodec_find_decoder_by_name获取到对应的解码。

那么问题来了,在FFmepg中MediaCodec对应的硬解码器是啥呢?我怎么知道avcodec_find_decoder_by_name应该传递的参数是什么呢?

咱们做NDK开发一定要学会妙用源码中configure这个文件,经过这个文件能够获取到很多咱们想要的装备信息,最简单的,假如咱们不知道有哪些可装备的编译信息, 则能够运用./configure --help进行检查。

同理,在FFmpeg的源码中,咱们能够经过指令行./configure --list-decoders检查它所支撑的解码器,如图仍是很多的,可是也并不是说都能直接运用的,因为大多数都是第三方的库, 一般需求在编译时翻开进行链接编译后才干正常运用。

FFmpeg调用MediaCodec解码

./configure --list-decoders输出太多了,咱们只关怀MediaCodec相关的,咱们能够运用grep过滤一下:

./configure  --list-decoders |grep mediacodec

输出如图,框起来的哪些便是能够作为函数avcodec_find_decoder_by_name参数的值,进行MediaCodec硬解码。

FFmpeg调用MediaCodec解码

  1. 装备硬解码器

要运用硬解码,你还得告诉解码器,你想要输出什么样的格式数据,这个便是装备硬解码器所要干的事情, 也便是说为了告诉解码器你想要获得的终究的YUV数据格式是什么?是NV12仍是NV21仍是其他?

在MediaCodec中硬解码的主要装备如下:

 // 装备硬解码器
                int i;
                for (i = 0;; i++) {
                    const AVCodecHWConfig *config = avcodec_get_hw_config(avCodec, i);
                    if (nullptr == config) {
                        LOGCATE("获取硬解码是装备失利");
                        return;
                    }
                    if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
                        config->device_type == AV_HWDEVICE_TYPE_MEDIACODEC) {
                        hw_pix_fmt = config->pix_fmt;
                        LOGCATE("硬件解码器装备成功");
                        break;
                    }
                }
  1. 初始化初始化mediacodec的buffer

咱们知道MediaCodec是基于队列的方法进行工作的,因而咱们还需求

// 硬件解码器初始化
    AVBufferRef *hw_device_ctx = nullptr;
    ret = av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_MEDIACODEC,
                           nullptr, nullptr, 0);
    if (ret < 0) {
        LOGCATE("Failed to create specified HW device");
        return;
    }
    avCodecContext->hw_device_ctx = av_buffer_ref(hw_device_ctx);

后续的其他过程就和软解码一样了,无非便是翻开解码器、读取视频包、将视频包送入解码器进行解码、从解码器中循环读取解码后的数据包等。 这些在之前的FFmpeg系列文章中已经介绍过很多了,这儿就不再负担了。

经过这么一个demo能够看出,万变不离其宗,FFMpeg6.0的硬解码比照曾经的形似仅仅省了一个av_jni_set_java_vm过程罢了,可是其内部是绕过了JNI调用MediaCodec的, 至于功能有了多少提升呢?感兴趣的同学们能够自行测验下。

下面是完好的代码:

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavcodec/codec.h>
#include <libavutil/avutil.h>
#include <libavutil/pixdesc.h>
}
AVPixelFormat hw_pix_fmt;
static enum AVPixelFormat get_hw_format(AVCodecContext *ctx,
                                        const enum AVPixelFormat *pix_fmts)
{
    const enum AVPixelFormat *p;
    for (p = pix_fmts; *p != -1; p++) {
        if (*p == hw_pix_fmt)
            return *p;
    }
    LOGD_E("FFDecoder","Failed to get HW surface format.\n");
    return AV_PIX_FMT_NONE;
}
void FFDecoder::decodeVideo(const char *videoPath, const char *yuvPath) {
    AVFormatContext *avFormatContext = avformat_alloc_context();
    int ret = avformat_open_input(&avFormatContext, videoPath, nullptr, nullptr);
    if (ret < 0) {
        LOGD_E("FFDecoder","翻开媒体文件失利");
        return;
    }
    avformat_find_stream_info(avFormatContext, nullptr);
    int video_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    if (video_index < 0) {
        LOGD_E("FFDecoder","找不到视频索引");
        return;
    }
    LOGD_E("FFDecoder","找到视频索引:%d", video_index);
    const AVCodec *avCodec = nullptr;
    AVCodecContext *avCodecContext = nullptr;
    AVPacket *avPacket = nullptr;
    AVFrame *avFrame = nullptr;
    FILE *yuv_file = nullptr;
    switch (avFormatContext->streams[video_index]->codecpar->codec_id) {
        // 这儿以h264为例
        case AV_CODEC_ID_H264:
            avCodec = avcodec_find_decoder_by_name("h264_mediacodec");
            if (nullptr == avCodec) {
                LOGD_E("FFDecoder","没有找到硬解码器h264_mediacodec");
                return;
            } else {
                // 装备硬解码器
                int i;
                for (i = 0;; i++) {
                    const AVCodecHWConfig *config = avcodec_get_hw_config(avCodec, i);
                    if (nullptr == config) {
                        LOGD_E("FFDecoder","获取硬解码是装备失利");
                        return;
                    }
                    if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
                        config->device_type == AV_HWDEVICE_TYPE_MEDIACODEC) {
                        hw_pix_fmt = config->pix_fmt;
                        LOGD_E("FFDecoder","硬件解码器装备成功");
                        break;
                    }
                }
                break;
            }
    }
    avCodecContext = avcodec_alloc_context3(avCodec);
    avcodec_parameters_to_context(avCodecContext,avFormatContext->streams[video_index]->codecpar);
    avCodecContext->get_format = get_hw_format;
    // 硬件解码器初始化
    AVBufferRef *hw_device_ctx = nullptr;
    ret = av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_MEDIACODEC,
                                 nullptr, nullptr, 0);
    if (ret < 0) {
        LOGD_E("FFDecoder","Failed to create specified HW device");
        return;
    }
    avCodecContext->hw_device_ctx = av_buffer_ref(hw_device_ctx);
    // 翻开解码器
    ret = avcodec_open2(avCodecContext, avCodec, nullptr);
    if (ret != 0) {
        LOGD_E("FFDecoder","解码器翻开失利:%s",av_err2str(ret));
        return;
    } else {
        LOGD_E("FFDecoder","解码器翻开成功");
    }
    avPacket = av_packet_alloc();
    avFrame = av_frame_alloc();
    yuv_file = fopen(yuvPath,"wb");
    while (true) {
        ret = av_read_frame(avFormatContext, avPacket);
        if (ret != 0) {
            LOGD_D("FFDecoder","av_read_frame end");
            // todo可能解码器内还有缓存的数据,需求avcodec_send_packet空包进行冲刷
            break;
        }
        if(avPacket->stream_index != video_index){
            av_packet_unref(avPacket);
            continue;
        }
        ret = avcodec_send_packet(avCodecContext,avPacket);
        if(ret == AVERROR(EAGAIN)){
            LOGD_E("FFDecoder","avcodec_send_packet EAGAIN");
        } else if(ret < 0){
            LOGD_E("FFDecoder","avcodec_send_packet fail:%s",av_err2str(ret));
            return;
        }
        av_packet_unref(avPacket);
        ret = avcodec_receive_frame(avCodecContext,avFrame);
        LOGD_E("FFDecoder","avcodec_receive_frame:%d",ret);
        while (ret == 0){
            LOGD_D("FFDecoder","获取解码数据成功:%s",av_get_pix_fmt_name(static_cast<AVPixelFormat>(avFrame->format)));
            LOGD_D("FFDecoder","linesize0:%d,linesize1:%d,linesize2:%d",avFrame->linesize[0],avFrame->linesize[1],avFrame->linesize[2]);
            LOGD_D("FFDecoder","width:%d,height:%d",avFrame->width,avFrame->height);
            ret = avcodec_receive_frame(avCodecContext,avFrame);
            // 假如解码出来的数据是nv12
            // 播映 ffplay -i d:/cap.yuv -pixel_format nv12 -framerate 25 -video_size 640x480
            // 写入y
            for(int j=0; j<avFrame->height; j++)
                fwrite(avFrame->data[0] + j * avFrame->linesize[0], 1, avFrame->width, yuv_file);
            // 写入uv
            for(int j=0; j<avFrame->height/2; j++)
                fwrite(avFrame->data[1] + j * avFrame->linesize[1], 1, avFrame->width, yuv_file);
        }
    }
    // 资源开释
    if (nullptr != avFormatContext) {
        avformat_free_context(avFormatContext);
        avFormatContext = nullptr;
    }
    if (nullptr != avCodecContext) {
        avcodec_free_context(&avCodecContext);
        avCodecContext = nullptr;
    }
    if (nullptr != avPacket) {
        av_packet_free(&avPacket);
        avPacket = nullptr;
    }
    if (nullptr != avFrame) {
        av_frame_free(&avFrame);
        avFrame = nullptr;
    }
    if(nullptr != yuv_file){
        fclose(yuv_file);
        yuv_file = nullptr;
    }
}

关于解码出来的YUV数据开释正常,咱们能够用adb将yuv文件数据从手机中拉出来到电脑端,运用ffplay指令播映一下验证即可。 正如代码所注释的,假定咱们解码得到的数据是NV12的,那么ffplay的播映指令便是:

ffplay -i yuv文件途径 -pixel_format nv12 -framerate 25 -video_size yuv宽x高
//假如解码出来的数据是nv12
//ffplay -i d:/cap.yuv -pixel_format nv12 -framerate 25 -video_size 640x480

推荐

音视频入门基础
C++进阶
NDK学习入门
安卓camera使用开发
ffmpeg系列
Opengl入门进阶
webRTC

重视我,一起前进,人生不止coding!!!