zoukankan      html  css  js  c++  java
  • 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示例

    实现代码

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


    from:https://www.jianshu.com/p/462752e352cb

  • 相关阅读:
    Windows Server 2003 服务器备份和恢复技巧
    查询表一张表的列名及字段类型
    aix 维护常用命令
    从 p12 格式 SSL 证书解出 pem 格式公钥私钥给 Postman 使用
    微信添加好友、加群的限制
    python requests 设置 proxy 和 SSL 证书
    blog post template(步骤类)
    post template(调查类)
    clip at cnblogs log
    《什么才是公司最好的福利》读后感
  • 原文地址:https://www.cnblogs.com/lidabo/p/15038672.html
Copyright © 2011-2022 走看看