欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

h264硬编解码ffmpeg(十一)

程序员文章站 2022-07-14 18:18:10
...

前言

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

视频硬解码相关流程

h264硬编解码ffmpeg(十一)

image.png

视频硬编码相关流程

h264硬编解码ffmpeg(十一)

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示例

实现代码

  • 公用代码

 

//
//  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三个目录下,可根据需要选择不同平台

相关标签: ffmpeg