h264硬编解码ffmpeg(十一)

前言

ffmpeg实现了软件解码,以及导入libx264等外部库实现软编码。同时它还对各个平台的硬编解码也进行了封装,提供了统一的调用接口。本文目的就是通过实现硬遍解码h264了解这些流程和接口

视频硬解码相关流程

image.png

视频硬编码相关流程

image.png

视频硬编解码相关函数及结构体

1、AVCodecContext
编解码结构体上下文,
对于硬解码,则需要设置如下两个变量
-get_format:此函数用于获取硬解码对应的像素格式,比如videotoolbox就是AV_PIX_FORMAT_VIDEOTOOLBOX
-hw_device_ctx:此函数用于设置硬解码的设备缓冲区引用,当此参数不为NULL时,解码将使用硬解码

设备缓冲区引用:AVBufferRef类型,它用于创建和管理帧缓冲区
帧缓冲区引用:AVBufferRef类型,管理编解码时GPU和CPU数据的交换冲区
帧缓冲区上下文:AVHWFramesContext类型,设置帧缓区的相关参数

对于videtoolbox和mediacodec的硬编码,使用流程和x264的软编码一样,不需要做额外的设置,对于VAAPI等其他类型的硬编码则有另外的使用流程,具体参考ffmpeg源码examples的vaapi_encode.c

2、AVBufferRef *av_buffer_ref(AVBufferRef *buf);
用于创建设备缓冲区
3、void av_buffer_unref(AVBufferRef **buf);
用于释放设备缓冲区,同时也会释放其管理的帧缓冲区
4、int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
将压缩数据AVPacket送入解码上下文缓冲区
5、int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
从解码上下文缓冲区获取解码后的数据AVFrame
6、int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags);
如果采用的硬件解码,则调用avcodec_receive_frame()函数后,解码后的数据还在GPU中,所以需要通过此函数将GPU中的数据转移到CPU中来
7、int avcodec_send_frame(AVCodecContext *avctx, const AVFrame *frame);
将未压缩数据AVFrame送入编码上下文缓冲区
8、int avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt);
从编码上下文缓冲区获取编码后的数据AVpacket

如果是videotoolbox和mediacodec进行硬编码,则没有设备缓冲区和帧缓冲区的设置,使用流程和x264一样,如果是vaapi等其它硬编码则有这样的概率,具体参考examples下的vaapi_encode.c示例

实现代码

  • 公用代码
扫描二维码关注公众号,回复: 14224421 查看本文章
//
//  hardDecoder.hpp
//  video_encode_decode
//
//  Created by apple on 2020/4/22.
//  Copyright © 2020 apple. All rights reserved.
//

#ifndef hardDecoder_hpp
#define hardDecoder_hpp
#include <string>
#include <stdio.h>
#include "cppcommon/CLog.h"
#include <sys/time.h>

extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/hwcontext.h>
#include <libavutil/pixfmt.h>
#include <libavutil/error.h>
}
using namespace::std;

class HardEnDecoder
{
public:
    HardEnDecoder();
    ~HardEnDecoder();
    
    void doDecode();
    void doEncode();
};

#endif /* hardDecoder_hpp */
  • 视频硬解码实现代码
enum AVPixelFormat hw_device_pixel;
enum AVPixelFormat hw_get_format(AVCodecContext *ctx,const enum AVPixelFormat *fmts)
{
    const enum AVPixelFormat *p;
    for (p = fmts; *p != AV_PIX_FMT_NONE; p++) {
        if (*p == hw_device_pixel) {
            return *p;
        }
    }
    
    return AV_PIX_FMT_NONE;
}

static void decode(AVCodecContext *ctx,AVPacket *packet)
{
    AVFrame *hw_frame = av_frame_alloc();
    AVFrame *sw_Frame = av_frame_alloc();
    AVFrame *tmp_frame = NULL;
    int ret = 0;
    static int sum = 0;
    if ((ret = avcodec_send_packet(ctx, packet))<0) {
        LOGD("avcodec_send_packet");
        return;
    }
    
    while (true) {
        ret = avcodec_receive_frame(ctx, hw_frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            LOGD("need more packet");
            av_frame_free(&hw_frame);
            return;
        } else if (ret < 0){
            return;
        }
#if USE_HARD_DEVICE
        if (hw_frame->format == hw_device_pixel) {
            // 如果采用的硬件加速剂,则调用avcodec_receive_frame()函数后,解码后的数据还在GPU中,所以需要通过此函数
            // 将GPU中的数据转移到CPU中来
            if ((ret = av_hwframe_transfer_data(sw_Frame, hw_frame, 0)) < 0) {
                LOGD("av_hwframe_transfer_data fail %d",ret);
                return;
            }
            LOGD("这里2222 解码成功 %d",sum);
            tmp_frame = sw_Frame;
        } else {
            LOGD("这里1111 解码成功 %d",sum);
            tmp_frame = hw_frame;
        }
#else
            
        LOGD("这里3333 解码成功 %d",sum);
#endif
        sum++;
    }
    
}

void HardEnDecoder::doDecode()
{
    string curFile(__FILE__);
    unsigned long pos = curFile.find("1-video_encode_decode");
    if (pos == string::npos) {
        LOGD("file not found");
        return;
    }
    string srcDic = curFile.substr(0,pos) + "filesources/";
    string srcPath = srcDic + "test_1280x720_3.mp4";
    
    AVCodecContext *decoder_Ctx = NULL;
    AVFormatContext *in_fmtCtx = NULL;
    int video_stream_index = -1;
    AVCodec *decoder = NULL;
    int ret = 0;
    enum AVHWDeviceType type = AV_HWDEVICE_TYPE_NONE;
    enum AVHWDeviceType print_type = AV_HWDEVICE_TYPE_NONE;
    AVBufferRef *hw_device_ctx = NULL;
    
    type = av_hwdevice_find_type_by_name("videotoolbox");
    // 遍历出设备支持的硬件类型;对于MAC来说就是AV_HWDEVICE_TYPE_VIDEOTOOLBOX
    while ((print_type = av_hwdevice_iterate_types(print_type)) != AV_HWDEVICE_TYPE_NONE) {
        LOGD("suport devices %s",av_hwdevice_get_type_name(print_type));
    }
    
    if ((ret = avformat_open_input(&in_fmtCtx,srcPath.c_str(),NULL,NULL)) < 0) {
        LOGD("avformat_open_input fail %d",ret);
        return;
    }
    if ((ret = avformat_find_stream_info(in_fmtCtx, NULL)) < 0) {
        LOGD("avformat_find_stream_info fail %d",ret);
        return;
    }
    
    // 最后一个参数目前未定义,填写0 即可
    // 找到指定流类型的流信息,并且初始化codec(如果codec没有值)
    if ((ret = av_find_best_stream(in_fmtCtx,AVMEDIA_TYPE_VIDEO,-1,-1,&decoder,0)) < 0) {
        LOGD("av_find_best_stream fail %d",ret);
        return;
    }
    video_stream_index = ret;
    
    // 根据解码器获取支持此解码方式的硬件加速计
    /** 所有支持的硬件解码器保存在AVCodec的hw_configs变量中。对于硬件编码器来说又是单独的AVCodec
     */
    for (int i=0;; i++) {
        const AVCodecHWConfig *hwcodec = avcodec_get_hw_config(decoder, i);
        if (hwcodec == NULL) break;
        
        // 可能一个解码器对应着多个硬件加速方式,所以这里将其挑选出来
        if (hwcodec->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX && hwcodec->device_type == type) {
            hw_device_pixel = hwcodec->pix_fmt;
        }
    }
    
    if ((decoder_Ctx = avcodec_alloc_context3(decoder)) == NULL) {
        LOGD("avcodec_alloc_context3 fail");
        return;
    }
    
    AVStream *video_stream = in_fmtCtx->streams[video_stream_index];
    // 给解码器赋值解码相关参数
    if (avcodec_parameters_to_context(decoder_Ctx,video_stream->codecpar) < 0) {
        LOGD("avcodec_parameters_to_context fail");
        return;
    }
    
#if USE_HARD_DEVICE
    // 配置获取硬件加速器像素格式的函数;该函数实际上就是将AVCodec中AVHWCodecConfig中的pix_fmt返回
    decoder_Ctx->get_format = hw_get_format;
    // 创建硬件加速器的缓冲区
    if (av_hwdevice_ctx_create(&hw_device_ctx,type,NULL,NULL,0) < 0) {
        LOGD("av_hwdevice_ctx_create fail");
        return;
    }
    /** 如果使用软解码则默认有一个软解码的缓冲区(获取AVFrame的),而硬解码则需要额外创建硬件解码的缓冲区
     *  这个缓冲区变量为hw_frames_ctx,不手动创建,则在调用avcodec_send_packet()函数内部自动创建一个
     *  但是必须手动赋值硬件解码缓冲区引用hw_device_ctx(它是一个AVBufferRef变量)
     */
    // 即hw_device_ctx有值则使用硬件解码
    decoder_Ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
#endif
    // 初始化并打开解码器上下文
    if (avcodec_open2(decoder_Ctx, decoder, NULL) < 0) {
        LOGD("avcodec_open2 fail");
        return;
    }
    
    
    /** 记录耗时
     *  1、使用硬件解码四次,耗时如下:10.65 s,10.66s,10.75s,10.68s
     *  2、使用软件解码四次,耗时如下:8.21s,8.02s,10.33s,8.00s
     *  结论:对于MAC来说,软件解码耗时比硬件少,但是时间波动大?
     */
    struct timeval btime;
    struct timeval etime;
    gettimeofday(&btime, NULL);
    AVPacket *packet = av_packet_alloc();
    while (av_read_frame(in_fmtCtx, packet) >= 0) {
        
        if (video_stream_index == packet->stream_index) {
            
            // 开始解码
            decode(decoder_Ctx,packet);
        }
        
        av_packet_unref(packet);
    }
    
    decode(decoder_Ctx,NULL);
    gettimeofday(&etime, NULL);
    LOGD("解码耗时 %.2f s",(etime.tv_sec - btime.tv_sec)+(etime.tv_usec - btime.tv_usec)/1000000.0f);
    
    avformat_close_input(&in_fmtCtx);
    avcodec_free_context(&decoder_Ctx);
    av_buffer_unref(&hw_device_ctx);
}
  • 视频硬编码实现代码
static void encode(AVCodecContext *codecCtx,AVFrame* frame,FILE *ouFile)
{
    static int sum = 0;
    int ret = 0;
    avcodec_send_frame(codecCtx, frame);
    AVPacket *packet = av_packet_alloc();
    while (true) {
        ret = avcodec_receive_packet(codecCtx, packet);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            LOGD("wait for more AVFrame");
            break;
        } else if (ret < 0) {
            exit(1);
        }
        
        // 编码成功
        LOGD("encode sucess size %d sum %d",packet->size,sum);
        sum++;
        // 对于编码后的h264数据 直接写入文件即可使用命令 ffplay 播放
        fwrite(packet->data, 1, packet->size, ouFile);
        av_packet_unref(packet);
    }
}

/** 实现yuv420P编码为h264;分别用h264_videotoolbox,libx264实现
 *  从代码上可以看到 采用videotoolbox进行硬件编码和采用libx264软件编码代码是一样的
 */
void HardEnDecoder::doEncode()
{
    string curFile(__FILE__);
    unsigned long pos = curFile.find("1-video_encode_decode");
    if (pos == string::npos) {
        LOGD("find pos fail");
        return;
    }
    string srcDic = curFile.substr(0,pos) + "filesources/";
    string srcPath = srcDic + "test_640x360_yuv420p.yuv";
    string dstPath = srcDic + "3-test.h264";
    // ===这些参数要与srcPath中的视频数据对应上===//
    int width = 640,height = 360,fps = 50;
    enum AVPixelFormat  sw_pix_format = AV_PIX_FMT_YUV420P;
    // ===这些参数要与srcPath中的视频数据对应上===//
    
    AVCodec *codec = NULL;
    AVCodecContext *codecCtx = NULL;
#if USE_ENCODER_VIDEOTOOLBOX
    /** 遇到问题:avcodec_find_encoder_by_name返回NULL,ffmpeg编译时h264_videotoolbox未编译进去;通过查看源码avcodec/codec_list.c即可知道未编译进去
     *  分析原因:对于编码器来说,要先使用硬件加速,则需要将对应的库加进去,就跟编译进libx264一样
     *  解决方案:编译ffmpeg时添加--enable_encoder=h264_videotoolbox;
    */
    codec = avcodec_find_encoder_by_name("h264_videotoolbox");
#else
    codec = avcodec_find_encoder_by_name("libx264");
#endif
    if (codec == NULL) {
        LOGD("avcodec_find_encoder_by_name is NULL");
        return;
    }
    
    codecCtx = avcodec_alloc_context3(codec);
    if (codecCtx == NULL) {
        LOGD("avcodec_alloc_context3 fail");
        return;
    }
    
    // 设置编码相关参数
    codecCtx->width = width;
    codecCtx->height = height;
    codecCtx->framerate = (AVRational){fps,1};
    codecCtx->time_base = (AVRational){1,fps};
    codecCtx->bit_rate = 0.96*1000000;
    codecCtx->gop_size = 10;
    codecCtx->pix_fmt = sw_pix_format;
    /** 遇到问题:编码得到的h264文件播放时提示"non-existing PPS 0 referenced"
     *  分析原因:未将pps sps 等信息写入
     *  解决方案:加入标记AV_CODEC_FLAG2_LOCAL_HEADER
     */
    codecCtx->flags |= AV_CODEC_FLAG2_LOCAL_HEADER;
#if !USE_ENCODER_VIDEOTOOLBOX
    // x264编码特有的参数
    if (codecCtx->codec_id == AV_CODEC_ID_H264) {
        av_opt_set(codecCtx->priv_data,"reset","slow",0);
    }
#endif
    
    if (avcodec_open2(codecCtx,codec,NULL) < 0) {
        LOGD("avcodec_open2() fail");
        avcodec_free_context(&codecCtx);
        return;
    }
    
    AVFrame *sw_frame = av_frame_alloc();
    sw_frame->width = width;
    sw_frame->height = height;
    sw_frame->format = codecCtx->pix_fmt;
    av_frame_get_buffer(sw_frame, 0);
    av_frame_make_writable(sw_frame);
    int frame_size = width * height;
    int frame_count = 0;
    FILE *inFile = fopen(srcPath.c_str(), "rb");
    FILE *ouFile = fopen(dstPath.c_str(), "wb+");
    while (true) {
        if (codecCtx->pix_fmt == AV_PIX_FMT_YUV420P) {
            // 读取数据之前先清掉之前数据
            memset(sw_frame->data[0], 0, frame_size);
            memset(sw_frame->data[1], 0, frame_size/4);
            memset(sw_frame->data[2], 0, frame_size/4);
            if (fread(sw_frame->data[0], 1, frame_size, inFile) <= 0) break;
            if (fread(sw_frame->data[1], 1, frame_size/4, inFile) <= 0) break;
            if (fread(sw_frame->data[2], 1, frame_size/4, inFile) <= 0) break;
        } else if (codecCtx->pix_fmt == AV_PIX_FMT_NV12 || codecCtx->pix_fmt == AV_PIX_FMT_NV21) {
            // 读取数据之前先清掉之前数据
            memset(sw_frame->data[0], 0, frame_size);
            memset(sw_frame->data[1], 0, frame_size/2);
            if (fread(sw_frame->data[0], 1, frame_size, inFile) <= 0) break;
            if (fread(sw_frame->data[1], 1, frame_size/2, inFile) <= 0) break;
        } else {
            LOGD("unsuport");
            break;
        }
        sw_frame->pts = frame_count;
        frame_count++;
        encode(codecCtx, sw_frame,ouFile);
    }
    
    // 刷新剩余未编码完的数据
    LOGD("文件数据读取完毕");
    encode(codecCtx, NULL,ouFile);
    
    // 释放资源
    avcodec_free_context(&codecCtx);
    av_frame_unref(sw_frame);
    fclose(inFile);
    fclose(ouFile);
}

备注:安卓平台硬编码ffmpeg目前还不支持

遇到问题

1、avcodec_find_encoder_by_name返回NULL,ffmpeg编译时h264_videotoolbox未编译进去;通过查看源码avcodec/codec_list.c即可知道未编译进去
分析原因:对于编码器来说,要先使用硬件加速,则需要将对应的库加进去,就跟编译进libx264一样
解决方案:编译ffmpeg时添加--enable_encoder=h264_videotoolbox;
2、编码得到的h264文件播放时提示"non-existing PPS 0 referenced"
分析原因:未将pps sps 等信息写入
解决方案:加入标记AV_CODEC_FLAG2_LOCAL_HEADER
codecCtx->flags |= AV_CODEC_FLAG2_LOCAL_HEADER;

项目地址

示例地址

示例代码位于cpp目录下文件
HardEnDecoder.hpp
HardEnDecoder.cpp

项目下示例可运行于iOS/android/mac平台,工程分别位于demo-ios/demo-android/demo-mac三个目录下,可根据需要选择不同平台

猜你喜欢

转载自blog.csdn.net/qq_21743659/article/details/109353003