FFmpeg之硬解码

  • Post author:
  • Post category:其他




导读

前面我们已经使用NDK编译出了FFmpeg并且已经集成到了Android Studio中去,相关文章:

NDK21编译ffmpeg5.0.1

众所周知,软解码虽然兼容性一流,但是却非常依赖CPU,所以性能消耗笔记大;硬解码使用内置的DSP芯片进行解码,性能高,但是兼容性一般。

虽说硬解码兼容性不太好,但是在实际开发中出于对性能的考虑我们依然会采用能硬解则硬解,不能硬解则软解兜底的方案。

我们知道安卓上可以使用MediaCodec进行硬解码,新版本FFmpeg内部也支持了MediaCodec硬解码,今天我们就使用FFMpeg在安卓上使用MediaCodec进行硬解码。


笔者测试的FFmpeg版本是最新的5.0.1,不同版本之间可以会有差异。



编译支持硬解码的FFmpeg

要编译支持硬解码的FFmpeg,在进行交叉编译时我们只需要打开以下几个属性即可:

--enable-hwaccels \
--enable-jni \
--enable-mediacodec \
--enable-decoder=h264_mediacodec \
--enable-decoder=hevc_mediacodec \
--enable-decoder=mpeg4_mediacodec \
--enable-hwaccel=h264_mediacodec \



使用FFMpeg进行硬解码

使用FFmpeg无论是硬解码还是软解码流程都是差不多的,对使用FFmpeg编解码API不熟悉的童鞋们可以回看之前发表的博客文章…

在FFmpeg源文件

hwcontext.c

中我们可以看出mediacodec对应的type类型是

AV_HWDEVICE_TYPE_MEDIACODEC

,这个

AV_HWDEVICE_TYPE_MEDIACODEC

很重要,

在配置硬解码器时都是需要使用到这个type。

static const char *const hw_type_names[] = {
    [AV_HWDEVICE_TYPE_CUDA]   = "cuda",
    [AV_HWDEVICE_TYPE_DRM]    = "drm",
    [AV_HWDEVICE_TYPE_DXVA2]  = "dxva2",
    [AV_HWDEVICE_TYPE_D3D11VA] = "d3d11va",
    [AV_HWDEVICE_TYPE_OPENCL] = "opencl",
    [AV_HWDEVICE_TYPE_QSV]    = "qsv",
    [AV_HWDEVICE_TYPE_VAAPI]  = "vaapi",
    [AV_HWDEVICE_TYPE_VDPAU]  = "vdpau",
    [AV_HWDEVICE_TYPE_VIDEOTOOLBOX] = "videotoolbox",
    [AV_HWDEVICE_TYPE_MEDIACODEC] = "mediacodec",
    [AV_HWDEVICE_TYPE_VULKAN] = "vulkan",
};

下面说说在FFMpeg配置硬解码器的大体步骤:

1、给FFMpeg设置虚拟机环境

首先在库加载函数

JNI_OnLoad

中调用FFmpeg的函数

av_jni_set_java_vm

,给FFMpeg设置虚拟机环境:

// 类库加载时自动调用
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reversed) {
    JNIEnv *env = NULL;
    // 初始化JNIEnv
    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_FALSE;
    }

    // 设置JavaVM,否则无法进行硬解码
    av_jni_set_java_vm(vm, nullptr);
    RegisterNativeMethods(env, "com/fly/ffmpeg/practice/ffmpeg/FFmpegHWDecoder",
                          const_cast<JNINativeMethod *>(hw_decoder_nativeMethod), sizeof(hw_decoder_nativeMethod) / sizeof (JNINativeMethod));
    // 返回JNI使用的版本
    return JNI_VERSION_1_4;
}

2、通过名字查找硬解码器

以h264为例,在安卓上它的硬解码器名字为

h264_mediacodec

,可以通过函数

avcodec_find_decoder_by_name("h264_mediacodec")

查找解码器,

如果返回空,一般就是不支持硬解码了。

3、配置硬解码器

这个配置主要是为了获取解码得到的YUV是什么格式的。

       // 配置硬解码器
                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;
                    }
                }

4、初始化mediacodec的buffer

    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) {
        LOGCATE("Failed to create specified HW device");
        return;
    }
    avCodecContext->hw_device_ctx = av_buffer_ref(hw_device_ctx);

5、打开解码器

和软解码一样,使用函数

avcodec_open2

打开解码器即可。后面的操作就是和软解码一样了。

从以上可以看出,硬解码和软解的区别就是硬解码需要多配置一点信息而已,下面贴一下主要代码:

#include "HWDecoder.h"
#include <log_cat.h>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavcodec/codec.h>
#include <libavutil/avutil.h>
#include <libavutil/pixdesc.h>
}

AVFormatContext *avFormatContext;
AVPacket *avPacket;
AVFrame *avFrame;
AVCodecContext *avCodecContext;
FILE *yuv_file;
HWDecoder::HWDecoder() {

}

HWDecoder::~HWDecoder() {
    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;
    }
}

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;
    }

    LOGCATE("Failed to get HW surface format.\n");
    return AV_PIX_FMT_NONE;
}

void HWDecoder::decode_video(const char *video_path, const char *yuv_path) {
    avFormatContext = avformat_alloc_context();
    int ret = avformat_open_input(&avFormatContext, video_path, nullptr, nullptr);
    if (ret < 0) {
        LOGCATE("打开媒体文件失败");
        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) {
        LOGCATE("找不到视频索引");
        return;
    }
    LOGCATE("找到视频索引:%d", video_index);

    const AVCodec *avCodec = 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) {
                LOGCATE("没有找到硬解码器h264_mediacodec");
                return;
            } else {
                // 配置硬解码器
                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;
                    }
                }
                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) {
        LOGCATE("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) {
        LOGCATE("解码器打开失败:%s",av_err2str(ret));
        return;
    } else {
        LOGCATE("解码器打开成功");
    }

    avPacket = av_packet_alloc();
    avFrame = av_frame_alloc();
    yuv_file = fopen(yuv_path,"wb");
    while (true) {
        ret = av_read_frame(avFormatContext, avPacket);
        if (ret != 0) {
            LOGCATE("av_read_frame end");
            break;
        }
        if(avPacket->stream_index != video_index){
            av_packet_unref(avPacket);
            continue;
        }
        ret = avcodec_send_packet(avCodecContext,avPacket);
        if(ret == AVERROR(EAGAIN)){
            LOGCATD("avcodec_send_packet EAGAIN");
        } else if(ret < 0){
            LOGCATE("avcodec_send_packet fail:%s",av_err2str(ret));
            return;
        }
        av_packet_unref(avPacket);
        ret = avcodec_receive_frame(avCodecContext,avFrame);
        LOGCATE("avcodec_receive_frame:%d",ret);
        while (ret == 0){
            LOGCATE("获取解码数据成功:%s",av_get_pix_fmt_name(static_cast<AVPixelFormat>(avFrame->format)));
            LOGCATE("linesize0:%d,linesize1:%d,linesize2:%d",avFrame->linesize[0],avFrame->linesize[1],avFrame->linesize[2]);
            LOGCATE("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);
        }
    }

}

解码成功将YUV写入文件后可以通过ffplay播放一下,看画面是否正常,怎么播放具体看注释。



遇到的问题

1、笔者在测试的过程中发现打开解码器报错:

Generic error in an external library

经查验代码发现是没有给FFmpeg设置JavaJVM,需要调用函数设置

av_jni_set_java_vm

JavaJVM参数即可。

2、如果解码得到的AVFrame的格式不是NV12或者NV21的话,表示数据有可能保存在GPU中,可以通过函数

av_hwframe_transfer_data

将数据取出到CPU。



推荐阅读


FFmpeg连载1-开发环境搭建



FFmpeg连载2-分离视频和音频



FFmpeg连载3-视频解码



FFmpeg连载4-音频解码



FFmpeg连载5-音视频编码



FFmpeg连载6-音频重采样



FFmpeg连载8-视频合并以及替换视频背景音乐实战



ffplay调试环境搭建



ffplay整体框架



ffplay数据读取线程



ffplay音视频解码线程



ffplay音视频同步



NDK21编译ffmpeg5.0.1

关注我,一起进步,人生不止coding!!!

微信扫码关注



版权声明:本文为u012944685原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。