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