C# 中的 H.264 CUDA 编码器 DirectShow 过滤器





5.00/5 (36投票s)
本文介绍了如何使用 C# 中的 NVIDIA 编码器 API 创建 H.264 视频编码器 DirectShow 过滤器
引言
此过滤器的实现基于我的 BaseClasses.NET 库,该库在我之前的文章中描述过(C# 中的纯 .NET DirectShow 过滤器)。 因此,您可以了解如何实现编码器过滤器。 该实现使用 NVIDIA CUDA 编码 API。
实现
过滤器的实现方式与前一篇文章中描述的相同。 它使用 TransformFilter
作为基类并覆盖其方法。 编码设置可以通过 IH264Encoder
接口进行配置
[ComVisible(true)]
[System.Security.SuppressUnmanagedCodeSecurity]
[Guid("089C95D9-7DAE-4916-BC52-54B45D3925D2")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IH264Encoder
{
[PreserveSig]
int get_Bitrate([Out] out int plValue);
[PreserveSig]
int put_Bitrate([In] int lValue);
[PreserveSig]
int get_RateControl([Out] out rate_control pValue);
[PreserveSig]
int put_RateControl([In] rate_control value);
[PreserveSig]
int get_MbEncoding([Out] out mb_encoding pValue);
[PreserveSig]
int put_MbEncoding([In] mb_encoding value);
[PreserveSig]
int get_Deblocking([Out,MarshalAs(UnmanagedType.Bool)] out bool pValue);
[PreserveSig]
int put_Deblocking([In,MarshalAs(UnmanagedType.Bool)] bool value);
[PreserveSig]
int get_GOP([Out,MarshalAs(UnmanagedType.Bool)] out bool pValue);
[PreserveSig]
int put_GOP([In,MarshalAs(UnmanagedType.Bool)] bool value);
[PreserveSig]
int get_AutoBitrate([Out,MarshalAs(UnmanagedType.Bool)] out bool pValue);
[PreserveSig]
int put_AutoBitrate([In,MarshalAs(UnmanagedType.Bool)] bool value);
[PreserveSig]
int get_Profile([Out] out profile_idc pValue);
[PreserveSig]
int put_Profile([In] profile_idc value);
[PreserveSig]
int get_Level([Out] out level_idc pValue);
[PreserveSig]
int put_Level([In] level_idc value);
[PreserveSig]
int get_SliceIntervals([Out] out int piIDR,[Out] out int piP);
[PreserveSig]
int put_SliceIntervals([In] ref int piIDR,[In] ref int piP);
}
通过该接口可以配置设置。 如何处理它的实现可以在过滤器属性页实现中找到。
NVIDIA CUDA 编码器 API
首先,我制作了 CUDA 视频编码器 API 的互操作
DllImport("nvcuvenc.dll")]
public static extern int NVCreateEncoder(out IntPtr pNVEncoder);
DllImport("nvcuvenc.dll")]
public static extern int NVDestroyEncoder(IntPtr hNVEncoder);
DllImport("nvcuvenc.dll")]
public static extern int NVIsSupportedCodec(IntPtr hNVEncoder, uint dwCodecType);
DllImport("nvcuvenc.dll")]
public static extern int NVIsSupportedCodecProfile(IntPtr hNVEncoder, uint dwCodecType, uint dwProfileType);
DllImport("nvcuvenc.dll")]
public static extern int NVSetCodec(IntPtr hNVEncoder, uint dwCodecType);
DllImport("nvcuvenc.dll")]
public static extern int NVGetCodec(IntPtr hNVEncoder, out uint pdwCodecType);
DllImport("nvcuvenc.dll")]
public static extern int NVIsSupportedParam(IntPtr hNVEncoder, NVVE_EncodeParams dwParamType);
DllImport("nvcuvenc.dll")]
public static extern int NVSetParamValue(IntPtr hNVEncoder, NVVE_EncodeParams dwParamType, IntPtr pData);
DllImport("nvcuvenc.dll")]
public static extern int NVGetParamValue(IntPtr hNVEncoder, NVVE_EncodeParams dwParamType, IntPtr pData);
DllImport("nvcuvenc.dll")]
public static extern int NVSetDefaultParam(IntPtr hNVEncoder);
DllImport("nvcuvenc.dll")]
public static extern int NVCreateHWEncoder(IntPtr hNVEncoder);
DllImport("nvcuvenc.dll")]
public static extern int NVGetSPSPPS(IntPtr hNVEncoder, IntPtr pSPSPPSbfr,
int nSizeSPSPPSbfr, out int pDatasize);
DllImport("nvcuvenc.dll")]
public static extern int NVEncodeFrame(IntPtr hNVEncoder,
[MarshalAs(UnmanagedType.LPStruct)] NVVE_EncodeFrameParams pFrmIn, uint flag, IntPtr pData);
DllImport("nvcuvenc.dll")]
public static extern int NVGetHWEncodeCaps();
DllImport("nvcuvenc.dll")]
public static extern void NVRegisterCB(IntPtr hNVEncoder, NVVE_CallbackParams cb, IntPtr pUserdata);
函数 NVRegiserCB
用于在 NVVE_CallbackParams
结构中注册编码器回调。 为此,我们需要为每个回调方法创建一个委托。
public delegate IntPtr PFNACQUIREBITSTREAM(ref int pBufferSize, IntPtr pUserdata);
public delegate void PFNRELEASEBITSTREAM(int nBytesInBuffer, IntPtr cb, IntPtr pUserdata);
public delegate void PFNONBEGINFRAME([MarshalAs(UnmanagedType.LPStruct)] NVVE_BeginFrameInfo pbfi, IntPtr pUserdata);
public delegate void PFNONENDFRAME([MarshalAs(UnmanagedType.LPStruct)] NVVE_EndFrameInfo pefi, IntPtr pUserdata);
[StructLayout(LayoutKind.Sequential)]
public struct NVVE_CallbackParams
{
public IntPtr pfnacquirebitstream;
public IntPtr pfnreleasebitstream;
public IntPtr pfnonbeginframe;
public IntPtr pfnonendframe;
}
在过滤器中,我们应该使用静态方法来遵循这些委托,并在类中创建委托变量 - 这是确保它们位于类范围内而不是局部范围内的主要事情。
private CUDA.PFNACQUIREBITSTREAM m_fnAcquireBitstreamDelegate = new CUDA.PFNACQUIREBITSTREAM(AcquireBitstream);
private CUDA.PFNRELEASEBITSTREAM m_fnReleaseBitstreamDelegate = new CUDA.PFNRELEASEBITSTREAM(ReleaseBitstream);
private CUDA.PFNONBEGINFRAME m_fnOnBeginFrame = new CUDA.PFNONBEGINFRAME(OnBeginFrame);
private CUDA.PFNONENDFRAME m_fnOnEndFrame = new CUDA.PFNONENDFRAME(OnEndFrame);
// initialization of the callback
CUDA.NVVE_CallbackParams _callback = new CUDA.NVVE_CallbackParams();
_callback.pfnacquirebitstream = Marshal.GetFunctionPointerForDelegate(m_fnAcquireBitstreamDelegate);
_callback.pfnonbeginframe = Marshal.GetFunctionPointerForDelegate(m_fnOnBeginFrame);
_callback.pfnonendframe = Marshal.GetFunctionPointerForDelegate(m_fnOnEndFrame);
_callback.pfnreleasebitstream = Marshal.GetFunctionPointerForDelegate(m_fnReleaseBitstreamDelegate);
// registering callbacks
CUDA.NVRegisterCB(m_hEncoder, _callback, Marshal.GetIUnknownForObject(this));
配置编码器
用于打开、配置和关闭编码器的主要方法
protected HRESULT OpenEncoder()
protected HRESULT ConfigureEncoder()
protected HRESULT CloseEncoder()
我将配置参数放入指定的结构中
protected struct Config
{
public uint Profile;
public CUDA.NVVE_FIELD_MODE Fields;
public CUDA.NVVE_RateCtrlType RateControl;
public bool bCabac;
public int IDRPeriod;
public int PPeriod;
public long Bitrate;
public bool bGOP;
public bool bAutoBitrate;
public bool bDeblocking;
}
要配置编码器,请使用一个 API NVSetParamValue
。 该 API 允许设置所有参数,并接受 IntPtr
作为值的参数,这是一个使用示例
IntPtr _ptr = IntPtr.Zero;
int[] aiSize = new int[2];
aiSize[0] = _bmi.Width;
aiSize[1] = Math.Abs(_bmi.Height);
_ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * aiSize.Length);
Marshal.Copy(aiSize,0,_ptr,aiSize.Length);
hr = (HRESULT)CUDA.NVSetParamValue(m_hEncoder, CUDA.NVVE_EncodeParams.NVVE_OUT_SIZE, _ptr);
Marshal.FreeCoTaskMem(_ptr);
接收和传递样本
首先,我们覆盖 OnReceive
方法。 在此方法中,我们填充 NVVE_EncodeFrameParams
结构并调用 NVEncodeFrame
API。 注意:我们不调用基类 OnReceive
方法。
public override int OnReceive(ref IMediaSampleImpl _sample)
{
{
AMMediaType pmt;
if (S_OK == _sample.GetMediaType(out pmt))
{
SetMediaType(PinDirection.Input,pmt);
Input.CurrentMediaType.Set(pmt);
pmt.Free();
}
}
CUDA.NVVE_EncodeFrameParams _params = new CUDA.NVVE_EncodeFrameParams();
_params.Height = m_nHeight;
_params.Width = m_nWidth;
_params.Pitch = m_nWidth;
if (m_SurfaceFormat == CUDA.NVVE_SurfaceFormat.YUY2 || m_SurfaceFormat == CUDA.NVVE_SurfaceFormat.UYVY)
{
_params.Pitch *= 2;
}
_params.PictureStruc = CUDA.NVVE_PicStruct.FRAME_PICTURE;
_params.SurfFmt = m_SurfaceFormat;
_params.progressiveFrame = 1;
_params.repeatFirstField = 0;
_params.topfieldfirst = 0;
_sample.GetPointer(out _params.picBuf);
_params.bLast = m_evFlush.WaitOne(0) ? 1 : 0;
HRESULT hr = (HRESULT)CUDA.NVEncodeFrame(m_hEncoder, _params, 0, m_dptrVideoFrame);
m_evReady.Reset();
return hr;
}
样本传递在编码器回调中执行。 在 AccureBitstream
中,我们从输出引脚分配器查询新样本并返回其缓冲区指针
private static IntPtr AcquireBitstream(ref int pBufferSize, IntPtr pUserData)
{
H264EncoderFilter pEncoder = (H264EncoderFilter)Marshal.GetObjectForIUnknown(pUserData);
pEncoder.m_pActiveSample = IntPtr.Zero;
pBufferSize = 0;
HRESULT hr = (HRESULT)pEncoder.Output.GetDeliveryBuffer(out pEncoder.m_pActiveSample,null,null, AMGBF.None);
IntPtr pBuffer = IntPtr.Zero;
if (SUCCEEDED(hr))
{
IMediaSampleImpl pSample = new IMediaSampleImpl(pEncoder.m_pActiveSample);
{
AMMediaType pmt;
if (S_OK == pSample.GetMediaType(out pmt))
{
pEncoder.SetMediaType(PinDirection.Output, pmt);
pEncoder.Output.CurrentMediaType.Set(pmt);
pmt.Free();
}
}
pSample.SetSyncPoint(pEncoder.m_bSyncSample);
pBufferSize = pSample.GetSize();
pSample.GetPointer(out pBuffer);
}
return pBuffer;
}
在 OnBeginFrame
回调例程中,我们保存样本的设置
private static void OnBeginFrame(CUDA.NVVE_BeginFrameInfo pbfi, IntPtr pUserData)
{
H264EncoderFilter pEncoder = (H264EncoderFilter)Marshal.GetObjectForIUnknown(pUserData);
pEncoder.m_bSyncSample = (pbfi != null && pbfi.nPicType == CUDA.NVVE_PIC_TYPE_IFRAME);
}
ReleaseBitstream
方法用于调用样本传递
private static void ReleaseBitstream(int nBytesInBuffer, IntPtr cb, IntPtr pUserData)
{
H264EncoderFilter pEncoder = (H264EncoderFilter)Marshal.GetObjectForIUnknown(pUserData);
IMediaSampleImpl pSample = new IMediaSampleImpl(pEncoder.m_pActiveSample);
pEncoder.m_pActiveSample = IntPtr.Zero;
if (pSample.IsValid)
{
long _start, _stop;
_start = pEncoder.m_rtPosition;
_stop = _start + pEncoder.m_rtFrameRate;
pEncoder.m_rtPosition = _stop;
pSample.SetTime(_start, _stop);
pSample.SetActualDataLength(nBytesInBuffer);
pEncoder.Output.Deliver(ref pSample);
pSample._Release();
}
}
滤镜概述
具有可用设置以进行配置的属性页。
过滤器支持以下媒体类型作为输入:NV12、YV12、UYVY、YUY2、YUYV、IYUV 以及格式 VideoInfo amd VideoInfo2。 对于平面类型,图片宽度应为 16 对齐,否则类型将被拒绝,因为过滤器不提供自己的分配器。
过滤器的输出类型是 VideoInfo 和 VideoInfo2 格式的 H264,带有 Mpeg2Video 格式的 AVC1 以及 NALU 大小长度 - 4。我制作了该过滤器,使其可以连接到 Avi Mux 过滤器以及常见的 mp4 mts flv 和 mkv 多路复用器。 我根据 DTS 而不是 PTS 设置媒体样本中的时间戳,但这对于好的解码器来说不是问题,并且即使在使用具有较大 IDR 周期的 closed gop 的情况下,也可以将视频嵌入到 AVI 中。
我使用该过滤器获得了良好的性能,并且在以 30 FPS 播放 1280x720 视频时,CPU 使用率仅为 4%。 这包括 VC-1 解码 H264 编码、解码和预览。
历史
- 2012-07-15 - 初始版本。