ffmpeg解碼的軟解及硬解(cuda和qsv)使用方法

對ffmpeg不是很熟悉,在使用的過程中遇到了很多坑,總結下,避免以後再遇到類似情況

版本兼容問題:

    本次使用的ffmpeg版本是4.2,解碼的調用方式爲:

int32_t iRet = -1;

// 最後一個包解碼完成後,需要取完解碼器中剩餘的緩存幀;
// 調用avcodec_send_packet時塞空包進去,;
// 解碼器就會知道所有包解碼完成,再調用avcodec_receive_frame時,將會取出緩存幀;
// AVPacket    packet;
// av_init_packet(&packet);
// pkt.data = NULL;
// pkt.size = 0;
// avcodec_send_packet(ctx, pkt);

iRet = avcodec_send_packet(ctx, pkt);
if (iRet != 0 && iRet != AVERROR(EAGAIN)) {
    get_ffmepg_err_str(iRet);
    if (iRet == AVERROR_EOF)
        iRet = 0;
    return iRet;
}

while (true) {
    // 每解出來一幀,丟到隊列中;
    iRet = avcodec_receive_frame(ctx, frame);
    if (iRet != 0) {
        if (iRet == AVERROR(EAGAIN)) {
            return 0;
        } else
            return iRet;
    }
    PushRenderBuffer();
    // 音頻解碼後,如果需要重採樣,也可以在此處進行resample;

}

以前的版本解碼方式爲:

int32_t iRet = -1;
iRet = avcodec_send_packet(ctx, pkt);
if (iRet != 0 && iRet != AVERROR(EAGAIN)) {
    get_ffmepg_err_str(iRet);
    if (iRet == AVERROR_EOF)
        iRet = 0;
    return iRet;
}

avcodec_decode_video2(pCodecCtx, frame, iGotPicture, pkt);

新舊版本更新時,注意接口的使用方法,新版本avcodec_send_packet一次,需要循環調用avcodec_receive_frame多次,返回EAGAIN後,結束當前這次的解碼,音頻解碼也是一樣

一、軟件解碼:

    先上一段代碼,再做下說明,這是解碼視頻文件,以AVStream的方式獲取文件信息,再創建解碼器

int32_t ret = avformat_open_input(&m_pAVFormatIC, pPath, NULL, NULL);
if (avformat_find_stream_info(m_pAVFormatIC, NULL) < 0) {
    return;
}
for (uint32_t i = 0; i < m_pAVFormatIC->nb_streams; i++) {
    switch (m_pAVFormatIC->streams[i]->codec->codec_type) {
        case AVMEDIA_TYPE_VIDEO:
            // 視頻流;
            AVCodec *find_codec = avcodec_find_decoder(m_pAVFormatIC->streams[i]->codec->codec_id);
            AVCodecContext *codec_ctx = avcodec_alloc_context3(find_codec);
            avcodec_parameters_to_context(codec_ctx, m_pAVFormatIC->streams[i]->codecpar);
            codec_ctx->opaque = this;
            codec_ctx->thread_count = 5;
            codec_ctx->thread_safe_callbacks = 1;
            // avcodec_receive_frame的frame數據內存交由自己申請,自己釋放,減少內存申請及拷貝;
            codec_ctx->get_buffer2 = DemuxStream::CustomFrameAllocBuffer;
            avcodec_open2(codec_ctx, find_codec, NULL);
            continue;
        case AVMEDIA_TYPE_AUDIO:
            // 音頻流;
            // 同上;
            continue;
    }
}

    1、m_pAVFormatIC->streams[i]->codec->codec_type 來判斷是否包含音頻和視頻數據,如果是mp3文件,codec_type有Video是表示MP3的封面圖片幀;

    2、avcodec_find_decoder和avcodec_alloc_context3創建的指針,不需要覆蓋stream中的指針,這是錯誤的用法,stream只是記錄當前文件的流信息,不要用來保存context,創建的context由自己來保管;

    3、avcodec_parameters_to_context,一定要做,這是把解析到的留信息設置到context,不需要在自己一個參數一個參數的設置;

    4、多線程軟解,thread_count不修改的話默認值是1,使用單個線程進行解碼,遇到一些大文件,比如4K,30幀以上的視頻流時,解碼速度是跟不上的,有兩種方案:多線程軟解或者硬件解碼(後面說)

         thread_count爲0,表示由ffmpeg調用最大線程數來進行軟解,我的電腦的6核12線程,被創建了13個線程進行解碼,實際上這會資源過剩,可以根據實際使用情況設置指定線程數量進行解碼,這裏我設置的是5,

        切記!在設置thread_count大於1的值時,一定要把thread_safe_callbacks設置爲1,否則的話必定會產生崩潰;

        默認時,thread_safe_callbacks是爲0,thread_count是1或者0的時候,都不需要這個標記

    5、自行分配avcodec_receive_frame中frame的數據內存

        調用avcodec_receive_frame獲取到解碼完成後的視頻幀,如果需要獲取視頻的數據流,需要將frame中的data拷貝到連續的內存地址中,具體方法:

uint8_t* AllocBufferByFrame(const AVFrame *frame)
{
    if (!frame)
        return nullptr;
    int32_t s = av_image_get_buffer_size((AVPixelFormat)frame->format,
                                         frame->width,
                                         frame->height, 1);
    if (s <= 0)
        return nullptr;
    return new uint8_t[s]{0};
}

int32_t FillBufferFromFrame(uint8_t* alloced_buffer, const AVFrame *frame)
{
    if (!alloced_buffer || !frame)
        return -1;

    uint8_t* dst_data[4]{};
    int dst_linesize[4]{};
    int32_t ret = av_image_fill_arrays(dst_data, dst_linesize, alloced_buffer,
        (AVPixelFormat)frame->format,
                                       frame->width,
                                       frame->height,
                                       1);
    if (ret <= 0)
        return ret;
    av_image_copy(dst_data, dst_linesize,
        (const uint8_t **)frame->data,
                  frame->linesize,
                  (AVPixelFormat)frame->format,
                  frame->width,
                  frame->height);
    return ret;
}

uint8_t *buffer = AllocBufferByFrame(frame);
FillBufferFromFrame(buffer, frame);
// 使用完buffer的地方,釋放buffer的內存;

        按照這種使用方法,同一幀的數據會申請兩份,解碼器中申請了一份,拷貝到buffer中一份,增加了內存消耗以及數據拷貝,ffmpeg提供瞭解碼時frame的內存由自己管理的回調接口,get_buffer2,這個回調接口在調用avcodec_receive_frame時,如果自定義了函數指針,將會調用自定義的函數接口,在接口內完成frame的data,linesize,buf的內容填充,在回調時,frame中的format,width,height已經被填充,可以直接拿來使用,注意:在get_buffer2的回調裏,flag是AV_GET_BUFFER_FLAG_REF(1),我們申請的內存會被其他frame複用,不能直接釋放,應該通過綁定的free接口來釋放,我這裏申請的是連續內存,回調free的接口每個平面都會調用,只有第一個平面也就是data[0]的時候,才能delete,方法如下:

void DemuxStream::CustomFrameFreeBuffer(void *opaque, uint8_t *data)
{
    //申請的空間是連續的,只有第一個data,也就是data[0]的時候再刪除,否則會崩潰;
    int i = (int)opaque;
    if (i == 0)
        delete data;
}

int DemuxStream::CustomFrameAllocBuffer(struct AVCodecContext *s, AVFrame *frame, int flags)
{
    int32_t size = av_image_get_buffer_size((AVPixelFormat)frame->format,
                                            frame->width,
                                            frame->height, 1);
    if (size <= 0)
        return -1;
    // 這是由自己申請的內存,avcodec_receive_frame使用完後要自己釋放;
    uint8_t *buffer = new uint8_t[size]{0};
    uint8_t* dst_data[AV_NUM_DATA_POINTERS]{};
    int dst_linesize[AV_NUM_DATA_POINTERS]{};
    int32_t ret = av_image_fill_arrays(dst_data, dst_linesize, buffer,
        (AVPixelFormat)frame->format, frame->width, frame->height, 1);
    if (ret < 0) {
        delete[] buffer;
        return ret;
    }
    for (int i = 0; i < AV_NUM_DATA_POINTERS; i++) {
        frame->linesize[i] = dst_linesize[i];
        frame->data[i] = dst_data[i];
        frame->buf[i] = av_buffer_create(frame->data[i],
                                         frame->linesize[i] * frame->height,
                                         DemuxStream::CustomFrameFreeBuffer,
                                         (void *)i, 0);
    }
    return 0;
}

// 解碼時僞代碼;
avcodec_send_packet(ctx, pkt);
while (true) {
    // 每解出來一幀,丟到隊列中;
    iRet = avcodec_receive_frame(ctx, frame);
    if (iRet != 0) {
        if (iRet == AVERROR(EAGAIN)) {
            return 0;
        } else
            return iRet;
    }
    push_render_buffer(frame);
    //使用完成後,調用av_frame_unref,內存回收會在CustomFrameFreeBuffer執行;
    //因爲自行申請的buffer,交給ffmpeg後,有可能會被複用,不能直接刪除buffer;
}

 

二、硬件解碼

    硬件解碼需要編譯的ffmpeg庫支持,具體編譯方法就不再此贅述,本次用到的硬件解碼爲英偉達cuda和英特爾的qsv,硬解解碼器的創建跟軟解有所不同,使用的過程也只是存在一點差異

    1、英偉達,cuda

       

AVPixelFormat DemuxStream::GetHwFormat(AVCodecContext * ctx, const AVPixelFormat * pix_fmts)
{
    const enum AVPixelFormat *p;
    DemuxStream *pThis = (DemuxStream *)ctx->opaque;
    for (p = pix_fmts; *p != -1; p++) {
        if (*p == pThis->m_AVStreamInfo.hw_pix_fmt) {
            return *p;
        }
    }

    fprintf(stderr, "Failed to get HW surface format.\n");
    return AV_PIX_FMT_NONE;
}
AVCodecContext * DemuxStream::GetCudaDecoder(AVStream *stream)
{
    int32_t ret = avformat_open_input(&m_pAVFormatIC, pPath, NULL, NULL);
    if (ret < 0)
        return nullptr;
    if (avformat_find_stream_info(m_pAVFormatIC, NULL) < 0)
        return nullptr;
    ret = av_find_best_stream(m_pAVFormatIC, AVMEDIA_TYPE_VIDEO, -1, -1, &find_codec, 0);
    if (ret < 0)
        return nullptr;
    for (int i = 0;; i++) {
        const AVCodecHWConfig *config = avcodec_get_hw_config(find_codec, i);
        if (!config) {
            // 沒找到cuda解碼器,不能使用;
            return nullptr;
        }
        if (config->device_type == AV_HWDEVICE_TYPE_CUDA) {
            // 找到了cuda解碼器,記錄對應的AVPixelFormat,後面get_format需要使用;
            m_AVStreamInfo.hw_pix_fmt = config->pix_fmt;
            m_AVStreamInfo.device_type = AV_HWDEVICE_TYPE_CUDA;
            break;
        }
    }
    AVCodecContext *decoder_ctx = avcodec_alloc_context3(find_codec);

    avcodec_parameters_to_context(decoder_ctx, m_pAVFormatIC->stream[video_index]->codecpar);
    decoder_ctx->opaque = this;
    decoder_ctx->get_format = DemuxStream::GetHwFormat;

    if (m_AVStreamInfo.hw_device_ctx) {
        av_buffer_unref(&m_AVStreamInfo.hw_device_ctx);
        m_AVStreamInfo.hw_device_ctx = NULL;
    }
    if (av_hwdevice_ctx_create(&m_AVStreamInfo.hw_device_ctx, m_AVStreamInfo.device_type,
                               NULL, NULL, 0) < 0) {
        fprintf(stderr, "Failed to create specified HW device.\n");
        // 創建硬解設備context失敗,不能使用;
        return nullptr;
    }
    decoder_ctx->hw_device_ctx = av_buffer_ref(m_AVStreamInfo.hw_device_ctx);
    avcodec_open2(decoder_ctx, find_codec, NULL);
}

        avcodec_get_hw_config,從當前硬解的配置中,遍歷尋找適用於當前AvCodec的cuda配置,找到後記錄cuda解碼後的AVPixelFormat,後面解碼器初始化時會使用到這個pix_fmt;

        設置get_format的回調接口,在回調接口裏面,會遍歷cuda解碼器支持的輸出格式,匹配記錄的pix_fmt纔可以使用,在get_format如果沒有找到匹配的硬解設置,可以返回指定類型的軟解輸出格式,解碼器會自動切換到軟解進行解碼

        初始化cuda解碼器之前,先創建硬解設備context,賦值AVCodecContext->hw_device_ctx爲其引用

        至此cuda解碼器創建完成,接下來是解碼獲取視頻幀,硬解獲得的視頻幀後續的處理方式與軟解是一樣的,硬解比軟解多了一步av_hwframe_transfer_data,在avcodec_receive_frame執行完成後,獲得的frame中的數據時GPU的數據,是不能直接拿出來用的,需要通過av_hwframe_transfer_data轉到內存數據,轉換完成後記得把frame的屬性通過av_frame_copy_props設置給hw_frame,這裏的硬解獲取視頻幀同樣適用於qsv硬解;

iRet = avcodec_send_packet(ctx, pkt);
if (iRet != 0 && iRet != AVERROR(EAGAIN)) {
    get_ffmepg_err_str(iRet);
    if (iRet == AVERROR_EOF)
        iRet = 0;
    return iRet;
}
while (true) {
    // 每解出來一幀,丟到隊列中;
    iRet = avcodec_receive_frame(ctx, frame);
    if (iRet != 0) {
        if (iRet == AVERROR(EAGAIN)) {
            return 0;
        } else
            return iRet;
    }
    if (m_pStreamInfo->hw_pix_fmt != AV_PIX_FMT_NONE &&
        m_pStreamInfo->device_type != AV_HWDEVICE_TYPE_NONE) {
        AVFrame *hw_frame = av_frame_alloc();
        iRet = av_hwframe_transfer_data(hw_frame, frame, 0);
        if (iRet < 0) {
            av_frame_unref(hw_frame);
            return iRet;
        }
        av_frame_copy_props(hw_frame, frame);
        // hw_frame中的data,就是硬解完成後的視頻幀數據;
    }
}

    2、英特爾硬解,qsv

        qsv的解碼器創建的方式與cuda不同,解碼獲取視頻幀內容與cuda完全一致,解碼的部分看上面,qsv的解碼器創建和cuda的區別在於,初始化AVCodecContext->hw_frames_ctx,cuda是在avcodec_open2之前創建,qsv是在get_format的回調裏創建,並且qsv需要做一些額外的設置;

        另外在查找codec_id的時候,需要加上“_qsv”的後綴,這個所支持的qsv的編碼格式,可以通過ffmpeg.exe查看到

        其他部分與cuda的創建基本一致

AVPixelFormat DemuxStream::GetHwFormat(AVCodecContext * ctx, const AVPixelFormat * pix_fmts)
{
    const enum AVPixelFormat *p;
    DemuxStream *pThis = (DemuxStream *)ctx->opaque;
    for (p = pix_fmts; *p != -1; p++) {
        if (*p == pThis->m_AVStreamInfo.hw_pix_fmt && *p == AV_PIX_FMT_QSV) {
            if (pThis->HwQsvDecoderInit(ctx) < 0)
                return AV_PIX_FMT_NONE;
            return *p;
        }
    }

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

int DemuxStream::HwQsvDecoderInit(AVCodecContext * ctx)
{
    DemuxStream *pThis = (DemuxStream *)ctx->opaque;
    AVHWFramesContext  *frames_ctx;
    AVQSVFramesContext *frames_hwctx;

    /* create a pool of surfaces to be used by the decoder */
    ctx->hw_frames_ctx = av_hwframe_ctx_alloc(pThis->m_qsv_device_ref);
    if (!ctx->hw_frames_ctx)
        return -1;
    frames_ctx = (AVHWFramesContext*)ctx->hw_frames_ctx->data;
    frames_hwctx = (AVQSVFramesContext *)frames_ctx->hwctx;

    frames_ctx->format = AV_PIX_FMT_QSV;
    frames_ctx->sw_format = ctx->sw_pix_fmt;
    frames_ctx->width = FFALIGN(ctx->coded_width, 32);
    frames_ctx->height = FFALIGN(ctx->coded_height, 32);
    frames_ctx->initial_pool_size = 16;

    frames_hwctx->frame_type = MFX_MEMTYPE_VIDEO_MEMORY_DECODER_TARGET;

    return av_hwframe_ctx_init(ctx->hw_frames_ctx);
}

AVCodecContext * DemuxStream::GetQsvDecoder(AVStream *stream)
{
    AVCodecContext *decoder_ctx = nullptr;
    int ret = av_hwdevice_ctx_create(&m_qsv_device_ref, AV_HWDEVICE_TYPE_QSV,
                                     "auto", NULL, 0);
    if (ret < 0) {

        goto failed;
    }
    AVCodec *find_decoder = avcodec_find_decoder(stream->codec->codec_id);
    if (!find_decoder) {
        goto failed;
    }
    find_decoder = avcodec_find_decoder_by_name((std::string(find_decoder->name) + "_qsv").c_str());
    if (!find_decoder) {
        goto failed;
    }

    for (int i = 0;; i++) {
        const AVCodecHWConfig *config = avcodec_get_hw_config(find_decoder, i);
        if (!config) {
            fprintf(stderr, "Decoder %s does not support device type %s.\n",
                    find_decoder->name, av_hwdevice_get_type_name(AV_HWDEVICE_TYPE_QSV));
            goto failed;
        }
        if (config->device_type == AV_HWDEVICE_TYPE_QSV) {
            m_AVStreamInfo.hw_pix_fmt = config->pix_fmt;
            m_AVStreamInfo.device_type = AV_HWDEVICE_TYPE_QSV;
            break;
        }
    }
    if (m_AVStreamInfo.device_type == AV_HWDEVICE_TYPE_NONE ||
        m_AVStreamInfo.hw_pix_fmt == AV_PIX_FMT_NONE)
        goto failed;
    decoder_ctx = avcodec_alloc_context3(find_decoder);
    if (!decoder_ctx)
        goto failed;
    if (avcodec_parameters_to_context(decoder_ctx, stream->codecpar) < 0)
        goto failed;
    decoder_ctx->opaque = this;
    decoder_ctx->get_format = DemuxStream::GetHwFormat;
    ret = avcodec_open2(decoder_ctx, NULL, NULL);
    if (ret < 0)
        goto failed;
    return decoder_ctx;
failed:
    m_AVStreamInfo.hw_pix_fmt = AV_PIX_FMT_NONE;
    m_AVStreamInfo.device_type = AV_HWDEVICE_TYPE_NONE;
    av_buffer_unref(&m_qsv_device_ref);
    m_qsv_device_ref = nullptr;
    avcodec_free_context(&decoder_ctx);
    return nullptr;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章