初始化硬解碼上下文
創建解碼數據緩沖區
這一步為了得到 LPDIRECT3DSURFACE9* 實例 m_pSurface,就是之前說過的那個數組。
// m_surfaceNums 為希望創建的緩沖區個數,單路視頻一個就夠了,太多可能顯存不夠用
m_pSurface = (LPDIRECT3DSURFACE9*)av_mallocz(m_surfaceNums * sizeof(LPDIRECT3DSURFACE9));
if (!m_pSurface)
{
return FALSE;
}
// 字節對齊
int surfaceAlignment = 0;
if (pCodecCtx->codec_id == AV_CODEC_ID_MPEG2VIDEO)
{
surfaceAlignment = 32;
}
else if (pCodecCtx->codec_id == AV_CODEC_ID_HEVC)
{
surfaceAlignment = 128;
}
else
{
surfaceAlignment = 16;
}
// 創建緩沖區
HRESULT hr = m_pDecoderService->CreateSurface(
FFALIGN(pCodecCtx->coded_width, surfaceAlignment), // 緩沖區寬
FFALIGN(pCodecCtx->coded_height, surfaceAlignment), // 緩沖區高
m_surfaceNums, // 緩沖區個數,這里可以設置為0,CreateVideoDecoder里面會重新設置個數
m_renderFormat, // 緩沖區格式
D3DPOOL_DEFAULT, // 緩沖區位置,D3DPOOL_DEFAULT--顯存
0, // 資源如何被使用
DXVA2_VideoDecoderRenderTarget, // 緩沖區為視頻解碼器渲染目標
m_pSurface, // 緩沖區數組指針
NULL); // 保留字
if (FAILED(hr))
{
return FALSE;
}
創建IDirectXVideoDecoder視頻解碼器
- 獲取當前GPU支持的解碼能力等級和渲染格式
BOOL GetDxva2FormatAndGuid(AVCodecContext *pCodecCtx, GUID & guid, D3DFORMAT & fmt)
{
// 獲取當前設備支持的解碼標准等級標識列表
GUID *guidList = NULL;
unsigned guidCount = 0;
HRESULT hr = m_pDecoderService->GetDecoderDeviceGuids(&guidCount, &guidList);
if (FAILED(hr))
{
VX_LOG_ERROR("Get hardware acclerate device guids failed!");
return FALSE;
}
for (int i = 0; ; i++)
{
if (NULL == guid2AVCodecID[i].guidID)
{
// 查到最后一個了直接退出循環
break;
}
const Guid2CodecID *mode = &guid2AVCodecID[i];
if (mode->codecID == pCodecCtx->codec_id)
{
for (uint32_t j = 0; j < guidCount; j++)
{
if (IsEqualGUID(*mode->guidID, guidList[j]))
{
// 獲取當前解碼標准下渲染器目標格式數組
D3DFORMAT *targetList = NULL;
UINT targetCount = 0;
hr = m_pDecoderService->GetDecoderRenderTargets(*mode->guidID, &targetCount, &targetList);
if (FAILED(hr))
{
VX_LOG_ERROR("Get support render format failed!");
return FALSE;
}
for (uint32_t j = 0; j < targetCount; j++)
{
if (targetList[j] == MKTAG('N', 'V', '1', '2'))
{
fmt = targetList[j];
guid = *mode->guidID;
break;
}
}
// 釋放內存資源
CoTaskMemFree(targetList);
}
}
}
}
CoTaskMemFree(guidList);
if (D3DFMT_UNKNOWN == fmt || GUID_NULL == guid)
{
return FALSE;
}
else
{
return TRUE;
}
}
- 獲取當前解碼等級下的配置信息
void CDxva2Decode::GetDecoderCfg(AVCodecContext *pCodecCtx, const GUID *pGuid,
const DXVA2_VideoDesc *pDesc, DXVA2_ConfigPictureDecode *pCfg)
{
unsigned cfgCount = 0, bestScore = 0;
DXVA2_ConfigPictureDecode *cfgList = NULL;
HRESULT hr = m_pDecoderService->GetDecoderConfigurations(*pGuid, pDesc, NULL, &cfgCount, &cfgList);
for (uint32_t i = 0; i < cfgCount; i++)
{
DXVA2_ConfigPictureDecode cfg = cfgList[i];
unsigned score;
if (cfg.ConfigBitstreamRaw == 1)
{
score = 1;
}
else if (pCodecCtx->codec_id == AV_CODEC_ID_H264 && cfg.ConfigBitstreamRaw == 2)
{
score = 2;
}
else
{
continue;
}
if (IsEqualGUID(cfg.guidConfigBitstreamEncryption, DXVA2_NoEncrypt))
{
score += 16;
}
if (score > bestScore)
{
bestScore = score;
*pCfg = cfg;
}
}
CoTaskMemFree(cfgList);
}
- 這一步為了得到 IDirectXVideoDecoder* 實例 m_pDxva2Decoder,亦即硬件解碼器。
if (!GetDxva2FormatAndGuid(pCodecCtx, m_decoderGuid, m_renderFormat))
{
// 不支持DXVA2加速
VX_LOG_ERROR("Do not support Dxva2!");
return FALSE;
}
// 設置解碼后的格式
DXVA2_VideoDesc desc = { 0 };
desc.SampleWidth = pCodecCtx->coded_width;
desc.SampleHeight = pCodecCtx->coded_height;
desc.Format = m_renderFormat;
// 獲取支持的配置
GetDecoderCfg(pCodecCtx, &m_decoderGuid, &desc, &m_config);
// 創建解碼器設備
HRESULT hr = m_pDecoderService->CreateVideoDecoder(m_decoderGuid, // 設備標識符
&desc, // 視頻內容描述
&m_config, // 解碼器配置
m_pSurface, // 渲染目標數組指針(解碼后的數據寫到這里)
m_surfaceNums, // 渲染目標數,必須大於0,
&m_pDxva2Decoder); // 解碼器
if (FAILED(hr))
{
return FALSE;
}
設置硬解碼上下文
// 這一步為了將解碼緩沖區數組傳給GetBufferCallBack回調函數
pCodecCtx->opaque = m_pSurface;
// 設置回調
pCodecCtx->get_buffer2 = GetBufferCallBack;
pCodecCtx->get_format = GetHwFormat;
// 單路視頻啟動多線程解碼,理解是啟用多個線程將待解碼數據送往GPU,因為數據從內存到顯存比較慢
pCodecCtx->thread_safe_callbacks = TRUE;
pCodecCtx->thread_count = 2;
// 為解碼器上下文申請硬件加速內存
pCodecCtx->hwaccel_context = av_mallocz(sizeof(struct dxva_context));
if (!pCodecCtx->hwaccel_context)
{
return FALSE;
}
// 設置硬件加速上下文
struct dxva_context *dxva2Ctx = (dxva_context *)pCodecCtx->hwaccel_context;
dxva2Ctx->cfg = &m_config;
dxva2Ctx->decoder = m_pDecoder;
dxva2Ctx->surface = m_pSurface;
dxva2Ctx->surface_count = m_surfaceNums;
// 對老的intel GPU 的支持
if (IsEqualGUID(m_decoderGuid, DXVADDI_Intel_ModeH264_E))
{
dxva2Ctx->workaround |= FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO;
}
解碼回調函數
- 解碼輸出格式回調
static AVPixelFormat GetHwFormat(AVCodecContext * pCodecCtx, const AVPixelFormat * pPixFmt)
{
// 因為采用的是DXVA2,所以這里直接寫死了
return AV_PIX_FMT_DXVA2_VLD;
}
-
解碼數據回調
此時解碼后的數據放在解碼緩沖數組里面,這里數組大小為1。單路視頻時 pFrame 地址為兩個固定地址切換,這些應該都是FFmpeg內部實現的。這里理解的不清楚,希望大神可以指點。
// 個人理解就是將LPDIRECT3DSURFACE9轉為(uint8_t *),同時得保證內存不會立即被釋放
static int GetBufferCallBack(AVCodecContext * pCodecCtx, AVFrame * pFrame, int flags)
{
if (pFrame->format != AV_PIX_FMT_DXVA2_VLD)
{
return -1;
}
// 獲取解碼后的數據,出於安全性不可直接訪問,這一步沒有內存拷貝
LPDIRECT3DSURFACE9 surface = ((LPDIRECT3DSURFACE9*)(pCodecCtx->opaque))[0];
// 將LPDIRECT3DSURFACE9轉為AVBuffer,內存地址不變,並返回AVBufferRef,並返回AVBufferRef供FFmpeg內部使用,這一步應該發生了內存拷貝
// 類似於智能指針,增加對surface的引用計數,當計數為0時FFmpeg會認為該幀數據丟棄掉。默認使用av_buffer_default_free釋放,
pFrame->buf[0] = av_buffer_create((uint8_t*)surface, 0, nullptr, nullptr, AV_BUFFER_FLAG_READONLY);
if (!pFrame->buf[0])
{
return AVERROR(ENOMEM);
}
// 這一步拿到最終可以顯示的數據,必須是data[3],此時surface應該是AVBuffer
pFrame->data[3] = (uint8_t *)surface;
return 0;
}