Delphi调用FFmpeg CUDA硬解码MP4视频出现画面阴影求助
问题:CUDA硬件加速解码MP4出现画面阴影
环境规格
- FFMPEG 4.3
- Delphi 10.3
- NVIDIA GeForce RTX 3050 Laptop GPU 4Gb DDR6
- Intel Core i5 11400H 16 Gb RAM
原始代码
unit UVideoDecoding; interface uses Vcl.Graphics, Winapi.Windows, System.SysUtils, System.Classes, libavcodec, libavdevice, libavfilter, libswresample, libavformat, libavutil, libswscale; type TDecoder = class protected fFormatCtx: PAVFormatContext; fCodec: PAVCodec; fCodecParams, fLocalCodecParams: PAVCodecParameters; fCodecCtx: libavcodec.PAVCodecContext; fPacket: PAVPacket; fFrameIn, fFrameOut: PAVFrame; fScaler: PSwsContext; fVideoStreamIndex: Integer; fHWPixelFormat: AVPixelFormat; fOutputWidth: Integer; fOutputHeight: Integer; function DecodeVideoPacket: Integer; public function Load(pFileName: string): Integer; end; implementation function TDecoder.Load(pFileName: string): Integer; var vCount: Integer; vLocalCodec: PAVCodec; vFileName: AnsiString; vType: AVHWDeviceType; vDecPackResponse: Integer; begin vType := av_hwdevice_find_type_by_name('cuda'); {vType = AV_HWDEVICE_TYPE_CUDA} fFormatCtx := avformat_alloc_context; vFileName := AnsiString(pFileName); avformat_open_input(fFormatCtx, PAnsiChar(vFileName), nil, nil); avformat_find_stream_info(fFormatCtx, nil); for vCount := 0 to fFormatCtx.nb_streams - 1 do begin fLocalCodecParams := fFormatCtx.streams[vCount].codecpar; case fLocalCodecParams.codec_id of AVCodecID.AV_CODEC_ID_H264: vLocalCodec := avcodec_find_decoder_by_name('h264_cuvid'); AVCodecID.AV_CODEC_ID_HEVC: vLocalCodec := avcodec_find_decoder_by_name('hevc_cuvid'); AVCodecID.AV_CODEC_ID_MJPEG: vLocalCodec := avcodec_find_decoder_by_name('mjpeg_cuvid'); AVCodecID.AV_CODEC_ID_MPEG4: vLocalCodec := avcodec_find_decoder_by_name('mpeg4_cuvid'); else vLocalCodec := avcodec_find_decoder(fLocalCodecParams.codec_id); end; if fLocalCodecParams.codec_type = AVMEDIA_TYPE_VIDEO then begin fVideoStreamIndex := vCount; fCodec := vLocalCodec; fCodecParams := fLocalCodecParams; end; end; {fCodec.name = h264_cuvid} fCodecCtx := avcodec_alloc_context3(fCodec); var vHWDeviceCtx: PAVBufferRef; try if av_hwdevice_ctx_create(vHWDeviceCtx, vType, nil, nil, 0) >= 0 then fCodecCtx.hw_device_ctx := av_buffer_ref(vHWDeviceCtx); finally av_buffer_unref(vHWDeviceCtx); end; avcodec_open2(fCodecCtx, fCodec, nil); { fCodecCtx.codec_id = AV_CODEC_ID_H264 fCodecCtx.pix_fmt = AV_PIX_FMT_MMAL } fFrameIn := av_frame_alloc; fFrameOut := av_frame_alloc; fFrameOut.format := Integer(AV_PIX_FMT_BGRA); fFrameOut.width := 640; fFrameOut.height := 480; //On getting sws_context I've tried srcFormat = AV_PIX_FMT_MMAL but the result was nil fScaler := sws_getContext(fCodecCtx.Width, fCodecCtx.Height, AV_PIX_FMT_NV12{fCodecCtx.pix_fmt}, fOutputWidth, fOutputHeight, AV_PIX_FMT_BGRA, SWS_BILINEAR, nil, nil, nil); fPacket := av_packet_alloc; while (av_read_frame(fFormatCtx, fPacket) >= 0) do begin if fPacket.stream_index = fVideoStreamIndex then begin vDecPackResponse := DecodeVideoPacket; av_packet_unref(fPacket); if vDecPackResponse < 0 then Break; end end; Exit(0); end; function TDecoder.DecodeVideoPacket: Integer; var vResponse: Integer; vBmp: Vcl.Graphics.TBitmap; vScaleResult, vSize: Integer; vBuffer: PByte; vSWFrame, vTMPFrame: pAVFrame; begin Result := 0; vResponse := avcodec_send_packet(fCodecCtx, fPacket); if vResponse < 0 then Exit(vResponse); while (vResponse >= 0) do begin vResponse := avcodec_receive_frame(fCodecCtx, fFrameIn); if (vResponse = AVERROR_EAGAIN) or (vResponse = AVERROR_EOF) then Break else if vResponse < 0 then Exit(vResponse); if vResponse >= 0 then begin vSWFrame := av_frame_alloc; if av_hwframe_transfer_data(vSWFrame, fFrameIn, 0) >= 0 then vTMPFrame := vSWFrame else vTMPFrame := fFrameIn; vSize := av_image_get_buffer_size(AVPixelFormat(vTMPFrame.format), vTMPFrame.width, vTMPFrame.height, 1); vBuffer := av_malloc(vSize); if Assigned(vBuffer) then av_image_copy_to_buffer(vBuffer, vSize, @vTMPFrame.data, @vTMPFrame.linesize, AVPixelFormat(vTMPFrame.format), vTMPFrame.width, vTMPFrame.height, 1); MoveMemory(vTMPFrame.data[0], vBuffer, vSize); vTMPFrame.data[0] := vTMPFrame.data[0] + vTMPFrame.linesize[0] * (fCodecCtx.height - 1); vTMPFrame.linesize[0] := vTMPFrame.linesize[0] * -1; vTMPFrame.data[1] := vTMPFrame.data[1] + vTMPFrame.linesize[1] * (fCodecCtx.height div 2 - 1); vTMPFrame.linesize[1] := vTMPFrame.linesize[1] * -1; vTMPFrame.data[2] := vTMPFrame.data[2] + vTMPFrame.linesize[2] * (fCodecCtx.height div 2 - 1); vTMPFrame.linesize[2] := vTMPFrame.linesize[2] * -1; vScaleResult := sws_scale(fScaler, @vTMPFrame.Data, @vTMPFrame.Linesize, 0, fCodecCtx.Height, @fFrameOut.data, @fFrameOut.Linesize); if vScaleResult <= 0 then begin Break; end; vBmp := Vcl.Graphics.TBitmap.Create; try vBmp.Height := fOutputHeight; vBmp.Width := fOutputWidth; vBmp.PixelFormat := TPixelFormat.pf32bit; MoveMemory(PByte(vBmp.ScanLine[vBmp.Height -1]), fFrameOut.data[0], fOutputHeight * fFrameOut.linesize[0]); //Renders BMP on main thread // if Assigned(fOnBitmap) then // fOnBitmap(vBmp); finally av_frame_unref(vSWFrame); av_frame_free(vTMPFrame); av_freep(@vBuffer); FreeAndNil(vBmp); Result := 1; end; end; end; end; end.
问题分析
- NV12格式处理错误:CUVID解码器输出的GPU帧通过
av_hwframe_transfer_data转换到CPU后是NV12格式,该格式只有Y(亮度)和UV(色度交织)两个平面,代码错误地将其当作YUV420P(三个分离平面)处理,修改了data[1]和data[2]的指针,导致色彩通道混乱,出现画面阴影。 - 无效的内存复制操作:手动将vBuffer复制回vTMPFrame.data[0]完全多余,
av_hwframe_transfer_data已经将帧数据正确写入vSWFrame,这步操作会破坏原始帧数据。 - fFrameOut未分配内存:仅设置格式、宽高但未调用
av_frame_get_buffer分配缓冲区,sws_scale写入时会触发未定义行为。 - 缩放上下文硬编码源格式:硬编码AV_PIX_FMT_NV12作为源格式,若解码器输出格式变化会导致缩放失败。
修复方案
1. 初始化fFrameOut时分配内存
在Load方法中,设置fFrameOut的格式、宽高后,添加内存分配逻辑:
fFrameOut.format := Integer(AV_PIX_FMT_BGRA); fFrameOut.width := 640; fFrameOut.height := 480; // 分配输出帧内存 if av_frame_get_buffer(fFrameOut, 1) < 0 then begin av_frame_free(fFrameOut); Exit(-1); end;
2. 动态创建缩放上下文
删除Load方法中创建fScaler的代码,改为在DecodeVideoPacket中获取vTMPFrame后动态创建或更新:
// 在DecodeVideoPacket中获取vTMPFrame后执行 if not Assigned(fScaler) or (AVPixelFormat(vTMPFrame.format) <> AVPixelFormat(fScaler.srcFormat)) then begin if Assigned(fScaler) then sws_freeContext(fScaler); fScaler := sws_getContext(vTMPFrame.width, vTMPFrame.height, AVPixelFormat(vTMPFrame.format), fFrameOut.width, fFrameOut.height, AV_PIX_FMT_BGRA, SWS_BILINEAR, nil, nil, nil); end;
3. 删除错误的内存复制和行反转操作
完全移除以下代码块:
vSize := av_image_get_buffer_size(AVPixelFormat(vTMPFrame.format), vTMPFrame.width, vTMPFrame.height, 1); vBuffer := av_malloc(vSize); if Assigned(vBuffer) then av_image_copy_to_buffer(vBuffer, vSize, @vTMPFrame.data, @vTMPFrame.linesize, AVPixelFormat(vTMPFrame.format), vTMPFrame.width, vTMPFrame.height, 1); MoveMemory(vTMPFrame.data[0], vBuffer, vSize); vTMPFrame.data[0] := vTMPFrame.data[0] + vTMPFrame.linesize[0] * (fCodecCtx.height - 1); vTMPFrame.linesize[0] := vTMPFrame.linesize[0] * -1; vTMPFrame.data[1] := vTMPFrame.data[1] + vTMPFrame.linesize[1] * (fCodecCtx.height div 2 - 1); vTMPFrame.linesize[1] := vTMPFrame.linesize[1] * -1; vTMPFrame.data[2] := vTMPFrame.data[2] + vTMPFrame.linesize[2] * (fCodecCtx.height div 2 - 1); vTMPFrame.linesize[2] := vTMPFrame.linesize[2] * -1;
4. 正确处理Bitmap行顺序
替换原Bitmap赋值代码,逐行复制BGRA数据以匹配Delphi Bitmap的行顺序:
// 替换原MoveMemory代码 var i: Integer; DestLine: PByte; SrcLine: PByte; begin for i := 0 to vBmp.Height - 1 do begin DestLine := vBmp.ScanLine[i]; SrcLine := fFrameOut.data[0] + i * fFrameOut.linesize[0]; MoveMemory(DestLine, SrcLine, vBmp.Width * 4); end; end;
修复后的DecodeVideoPacket核心代码
function TDecoder.DecodeVideoPacket: Integer; var vResponse: Integer; vBmp: Vcl.Graphics.TBitmap; vScaleResult: Integer; vSWFrame, vTMPFrame: pAVFrame; i: Integer; DestLine: PByte; SrcLine: PByte; begin Result := 0; vResponse := avcodec_send_packet(fCodecCtx, fPacket); if vResponse < 0 then Exit(vResponse); while (vResponse >= 0) do begin vResponse := avcodec_receive_frame(fCodecCtx, fFrameIn); if (vResponse = AVERROR_EAGAIN) or (vResponse = AVERROR_EOF) then Break else if vResponse < 0 then Exit(vResponse); if vResponse >= 0 then begin vSWFrame := av_frame_alloc; try if av_hwframe_transfer_data(vSWFrame, fFrameIn, 0) >= 0 then vTMPFrame := vSWFrame else vTMPFrame := fFrameIn; // 动态创建/更新缩放上下文 if not Assigned(fScaler) or (AVPixelFormat(vTMPFrame.format) <> AVPixelFormat(fScaler.srcFormat)) then begin if Assigned(fScaler) then sws_freeContext(fScaler); fScaler := sws_getContext(vTMPFrame.width, vTMPFrame.height, AVPixelFormat(vTMPFrame.format), fFrameOut.width, fFrameOut.height, AV_PIX_FMT_BGRA, SWS_BILINEAR, nil, nil, nil); end; if not Assigned(fScaler) then begin Break; end; vScaleResult := sws_scale(fScaler, @vTMPFrame.Data, @vTMPFrame.Linesize, 0, vTMPFrame.Height, @fFrameOut.data, @fFrameOut.Linesize); if vScaleResult <= 0 then begin Break; end; vBmp := Vcl.Graphics.TBitmap.Create; try vBmp.Height := fOutputHeight; vBmp.Width := fOutputWidth; vBmp.PixelFormat := TPixelFormat.pf32bit; // 逐行复制BGRA数据到Bitmap for i := 0 to vBmp.Height - 1 do begin DestLine := vBmp.ScanLine[i]; SrcLine := fFrameOut.data[0] + i * fFrameOut.linesize[0]; MoveMemory(DestLine, SrcLine, vBmp.Width * 4); end; // 渲染逻辑 // if Assigned(fOnBitmap) then // fOnBitmap(vBmp); finally FreeAndNil(vBmp); end; Result := 1; finally av_frame_unref(vSWFrame); av_frame_free(vSWFrame); end; end; end; end;
额外注意事项
- 确保FFmpeg库编译时启用了CUDA支持,且版本与GPU驱动兼容。
- 解码完成后需释放所有FFmpeg资源(
avformat_close_input、avcodec_free_context、sws_freeContext等),避免内存泄漏。
内容的提问来源于stack exchange,提问作者Edimar
相关产品推荐
相关产品推荐

