You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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.

问题分析

  1. NV12格式处理错误:CUVID解码器输出的GPU帧通过av_hwframe_transfer_data转换到CPU后是NV12格式,该格式只有Y(亮度)和UV(色度交织)两个平面,代码错误地将其当作YUV420P(三个分离平面)处理,修改了data[1]和data[2]的指针,导致色彩通道混乱,出现画面阴影。
  2. 无效的内存复制操作:手动将vBuffer复制回vTMPFrame.data[0]完全多余,av_hwframe_transfer_data已经将帧数据正确写入vSWFrame,这步操作会破坏原始帧数据。
  3. fFrameOut未分配内存:仅设置格式、宽高但未调用av_frame_get_buffer分配缓冲区,sws_scale写入时会触发未定义行为。
  4. 缩放上下文硬编码源格式:硬编码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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 08:45:26