65.9K
CodeProject 正在变化。 阅读更多。
Home

在C#中基于AMD GPU的H264编码器DirectShow滤镜

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2024年1月24日

CPOL

21分钟阅读

viewsIcon

4226

downloadIcon

189

使用AMD的AMF SDK API在C#中制作H.264视频编码器DirectShow滤镜

目录

引言

每个GPU供应商都为其产品的使用提供了自己的功能。在本文中,我将展示如何使用AMD的高级媒体框架(AMF)SDK来实现H264 DirectShow视频编码器滤镜。此滤镜的实现采用C#完成,并基于我上一篇文章(C#中的纯.NET DirectShow滤镜)中描述的BaseClasses.NET库。

高级媒体框架 (AMF) SDK 封装器

高级媒体框架 (AMF) SDK 为开发人员在 AMD GPU 设备上执行多媒体处理提供了最佳接口。要在 .NET 中使用该 SDK,我们需要封装对象和函数。封装器的起点是初始化函数:AMFInit()。它从 x86 平台的 *amfrt32.dll* 和 x64 平台的 *amfrt64.dll* 导出。一个好的方法是为这两个平台创建该函数的封装器,并根据运行时目标调用其中一个,因为在 .NET 中我们可以有“任何 CPU”配置。

[DllImport("amfrt64.dll", CharSet = CharSet.Ansi, 
CallingConvention = CallingConvention.Cdecl, EntryPoint = "AMFInit")]
private static extern AMF_RESULT AMFInit64(ulong version, out IntPtr ppFactory);

[DllImport("amfrt32.dll", CharSet = CharSet.Ansi, 
CallingConvention = CallingConvention.Cdecl, EntryPoint = "AMFInit")]
private static extern AMF_RESULT AMFInit32(ulong version, out IntPtr ppFactory);

我们有不同的函数名称和不同的导出DLL名称,但入口点属性值是相同的。根据目标运行时平台,我们将调用AMFInit32()AMFInit64()函数。要检查使用哪个运行时,我们检查IntPtr.Size值。

string sModule = "";
AMFInit_Fn AMFInit = null;
if (IntPtr.Size == 4)
{
    sModule = "amfrt32.dll";
    AMFInit = AMFInit32;
}
if (IntPtr.Size == 8)
{
    sModule = "amfrt64.dll";
    AMFInit = AMFInit64;
}
s_hDll = LoadLibrary(sModule);
if (s_hDll != null)
{
    var result = AMFInit(AMF_FULL_VERSION, out m_hFactory);
}

为了正确检查系统上是否存在 SDK 运行时 DLL,我们使用 LoadLibrary 函数并在启动时尝试加载模块,如果成功,我们继续初始化。从初始化函数中,我们获得了工厂对象指针。与本机 AMF SDK 中一样,我们将初始化作为单例模式与 AMFRoot 对象和 GetFactory() static 方法一起进行。

AMFRoot

AMFRoot 类是每个 AMF SDK 导入对象的基础。构造函数中的引用计数器在 IDisposable 接口实现的 Dispose 方法中递增和递减。一旦计数器变为零,这意味着最新导出的对象已释放,运行时 DLL 将被卸载。

AMFObject

表示 AMF SDK 对象的基本类是 AMFObject 类。它嵌入了对底层原生 AMF 对象的 IntPtr 句柄引用,并包含相等运算符。它是一个中间类,具有 protected 构造函数。

AMFInterface

所有 AMF 封装器对象都与它们在 AMF SDK 中存在的名称相同。因此,基础 AMF 接口封装器类名为 AMFInterface,它实现了同名的底层 AMF 接口。该类控制对象引用并管理从该对象请求其他支持的接口。所有其他 AMF 接口都继承自该类。底层对象的导出函数通过其 Vtable 直接按索引访问。其方式与 DirectShow 基类 .NET 封装器的封送接口实现方式相同。为此,我们有一个函数 GetProcDelegate(),它通过给定接口指针的索引返回一个方法委托。

protected T GetProcDelegate<T>(int nIndex) where T : class
{
    IntPtr pFunc = IntPtr.Zero;
    lock (this)
    {
        if (m_hHandle == IntPtr.Zero) return null;
        IntPtr pVtable = Marshal.ReadIntPtr(m_hHandle);
        pFunc = Marshal.ReadIntPtr(pVtable, nIndex * IntPtr.Size);
    }
    return (Marshal.GetDelegateForFunctionPointer(pFunc, typeof(T))) as T;
}

因此,AMFInterface 类的 QueryInterface() 方法将具有以下实现。

private AMF_RESULT QueryInterface(Guid iid, out IntPtr p)
{
    AMF_RESULT result = AMF_RESULT.NO_INTERFACE;
    p = IntPtr.Zero;
    lock (this)
    {
        if (m_hHandle != IntPtr.Zero)
        {
            var Proc = GetProcDelegate<FNQueryInterface>(2);
            result = Proc(m_hHandle, ref iid, out p);
        }
    }
    return result;
}

以及实际的 .NET 类型。

public T QueryInterface<T>(Guid _guid) where T : AMFInterface, new()
{
    IntPtr p;
    if (AMF_RESULT.OK == QueryInterface(_guid, out p))
    {
        T pT = new T();
        AMFInterface _interface = (AMFInterface)pT;
        _interface.m_hHandle = p;
        return pT;
    }
    return null;
}

AMFPropertyStorage

允许访问对象属性集合的主要接口是 AMFPropertyStorage。通过此接口,我们应用编码器的设置并配置编码器输入表面的属性。同时,我们还检索结果缓冲区的参数。AMFPropertyStorage 是核心对象,因为此接口是大多数 AMF 对象的基础。
在内部,AMFPropertyStorage 使用指定的变体类型结构进行操作。在 .NET 实现中,我们设法简化并隐藏了这些可变内容。主方法 GetProperty()SetProperty() 接受不同的值类型。根据该类型,它准备内部可变结构并调用实际 AMFPropertyStorage 接口的基方法。
对现有值的访问也作为索引属性完成。

AMFData

AMFData 是表示 GPU 或 CPU 内存缓冲区的抽象类的接口。它继承自 AMFPropertyStorage 接口。通过 DataType 字段,可以确定当前 AMFData 实例的基础对象是什么。它可以是 AMFSurfaceAMFBuffer——这两个对于我们的实现是必需的。

AMFBuffer

继承自 AMFData 的接口,提供对 GPU 或 CPU 内存中无序缓冲区数据的访问。

AMFPlane

继承自 AMFInterface 的接口,提供对表面单个平面的访问。指向 AMFPlane 接口的指针可以使用 AMFSurface 接口的 GetPlane() 方法的变体获得。每个 AMFSurface 对象至少包含一个平面。表面中的平面数量由表面格式决定。

AMFSurface

该接口抽象了一个包含 GPU 可访问的 2D 图像的内存缓冲区。缓冲区的结构取决于表面类型和格式。与表面关联的内存缓冲区可以存储在 GPU 或主机内存中,并由一个或多个可通过 AMFPlane 接口访问的平面组成。

AMFContext

用于为 AMF 功能创建特定于设备的资源的接口。它通过通用 API 访问抽象了底层平台特定技术。在我们的实现中,我们只公开了创建编码器所需的几个方法。该接口继承自 AMFPropertyStorage

AMFComponent

AMF 组件功能的实现,可以处理输入数据并提供输出。此接口派生自 AMFPropertyStorageEx,即额外的属性存储,它同时继承自 AMFPropertyStorage。该接口公开了几个用于初始化和提交输入以及生成输出 AMFData 对象的方法。AMF SDK 中指定的组件数据处理的基本流程图如下:

AMFFactory

AMFFactory 接口是创建 AMF 对象的入口点。AMFFactory 继承自 AMFObject,而不是像其他 AMF 对象那样派生自 AMFInterface。我们从 AMFRoot 类的 static 方法 GetFactory() 获取 AMFFactory 实例。封装器对象只公开了我们需要的两个方法。它们是用于创建 AMFContext 对象实例的 CreateContext() 和用于创建编码器组件的 CreateComponent()

编码器初始化

既然我们已经为所有必要的功能准备好了封装器,那么我们需要实现创建编码器对象并初始化其属性。

在滤镜实现中,我们有三个辅助方法;它们用于创建编码器对象、初始化其属性并销毁它:OpenEncoder()ConfigureEncoder()CloseEncoder()
OpenEncoder() 用于创建编码器实例。同时,它会调用 ConfigureEncoder() 方法,我们在其中应用编码器设置和输入流参数。CloseEncoder() 方法用于处置编码器变量并释放资源。访问编码器变量被临界区包围,以避免多线程问题。
如前所述,我们使用 AMFFactory 对象创建 AMFContext 接口的实例。AMFContext 应使用我们打算使用的指定图形技术进行初始化。我们的封装器只导出了 DirectX11 初始化方法,但在您的实现中,您可以为 AMF SDK 支持的其他技术更改功能。要了解更多信息,您可以查看 SDK 文档AMFContext 接口的方法。根据所选的图形技术,编码器组件使用不同的底层类型。例如,如果您使用 DirectX11 渲染某些内容,您可以使用此技术初始化 AMFContext 并为编码提供您自己的渲染目标,而无需任何复制要求,因为 AMFContext 包含允许您从所选技术的现有资源创建对象的方法。

// Create Encoder Objects
protected HRESULT OpenEncoder()
{
    lock (this)
    {
        if (m_Encoder != null) return NOERROR;
        AMF.AMF_RESULT result;
        AMF.AMFFactory factory = null;
        try
        {
            // Get the factory object
            result = AMF.AMFRoot.GetFactory(out factory);
            if (result == AMF.AMF_RESULT.OK)
            {
                // Create context
                result = factory.CreateContext(out m_Context);
            }
            if (result == AMF.AMF_RESULT.OK)
            {
                result = AMF.AMF_RESULT.NOT_SUPPORTED;
                if (m_DecoderType == AMF.AMF_MEMORY_TYPE.DX11)
                {
                    // Initialize context type
                    result = m_Context.InitDX11();
                }
            }
            if (result == AMF.AMF_RESULT.OK)
            {
                // Create Encoder Component
                result = factory.CreateComponent(m_Context, 
                                                AMF.AMFVideoEncoder.VCE_AVC, out m_Encoder);
            }
        }
        finally
        {
            if (factory != null)
            {
                factory.Dispose();
            }
        }

        if (result != AMF.AMF_RESULT.OK)
        {
            CloseEncoder();
            return E_FAIL;
        }
        // Configure Encoder
        HRESULT hr = ConfigureEncoder();
        if (FAILED(hr))
        {
            CloseEncoder();
            return hr;
        }
        // Mark that we need to send sps pps data first
        m_bFirstSample = true;
        // Initialize Encoder Object
        result = m_Encoder.Init(m_SurfaceFormat, m_nWidth, m_nHeight);
        if (result != AMF.AMF_RESULT.OK)
        {
            CloseEncoder();
            return E_FAIL;
        }
    }
    return S_OK;
}

在初始化 AMFContext 对象后,我们创建编码器 AMFComponent 的实例。这通过 AMFFactoryCreateComponent() 方法完成。我们指定了 "AMFVideoEncoderVCE_AVC" 组件 string,它标识了 H264 编码器组件类型。一旦编码器组件创建成功,我们调用我们准备好的 ConfigureEncoder() 方法来设置其参数。

// Setup Encoder settings
protected HRESULT ConfigureEncoder()
{
    if (!Input.IsConnected) return VFW_E_NOT_CONNECTED;
    BitmapInfoHeader _bmi = Input.CurrentMediaType;
    if (Output.IsConnected)
    {
        m_bAVC = (Output.CurrentMediaType.subType == MEDIASUBTYPE_AVC);
    }
    AMF.AMF_RESULT result;
    lock (this)
    {
        result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.USAGE, 
                                      (long)AMF.AMF_VIDEO_ENCODER_USAGE.TRANSCODING);
        result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.QUALITY_PRESET, 
                                      (int)m_Config.Preset);
        result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.FRAMESIZE, 
                                            new AMF.AMFSize(m_nWidth, m_nHeight));
        result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.SCANTYPE, 
                                      (int)AMF.AMF_VIDEO_ENCODER_SCANTYPE.PROGRESSIVE);
        result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.RATE_CONTROL_METHOD, 
                                      (int)m_Config.RateControl);
        result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.DE_BLOCKING_FILTER, 
                                       m_Config.bDeblocking);
        result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.IDR_PERIOD, 
                                      (int)m_Config.IDRPeriod);
        if ((m_Config.Profile & 0xff) != 0)
        {
            result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.PROFILE, 
                                            (int)(m_Config.Profile & 0xff));
        }
        if (((m_Config.Profile >> 8) & 0xff) != 0)
        {
            result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.PROFILE_LEVEL, 
                                            (int)((m_Config.Profile >> 8) & 0xff));
        }
        result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.B_PIC_PATTERN, 
                                       m_Config.BPeriod);
        result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.ADAPTIVE_MINIGOP, 
                                       m_Config.bGOP);
              
        long _rate = 0;
        if (_rate == 0)
        {
            VideoInfoHeader _vih = Input.CurrentMediaType;
            if (_vih != null)
            {
                _rate = _vih.AvgTimePerFrame;
            }
        }
        if (_rate == 0)
        {
            VideoInfoHeader2 _vih = Input.CurrentMediaType;
            if (_vih != null)
            {
                _rate = _vih.AvgTimePerFrame;
            }
        }
        if (_rate != 0)
        {
            long a = UNITS;
            long b = _rate;
            long c = a % b;
            while (c != 0)
            {
                a = b;
                b = c;
                c = a % b;
            }
            result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.FRAMERATE, 
                                           new AMF.AMFRate((int)(UNITS / b), 
                                           (int)(_rate / b)));
        }
        m_rtFrameRate = _rate;
        if (m_nWidth != 0 && m_nHeight != 0)
        {
            int a = m_nWidth;
            int b = m_nHeight;
            int c = a % b;
            while (c != 0)
            {
                a = b;
                b = c;
                c = a % b;
            }
            result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.ASPECT_RATIO, 
                                           new AMF.AMFRatio((uint)(m_nWidth / b), 
                                           (uint)(m_nHeight / b)));
        }
        long lRecomended = ((long)m_nWidth * (long)m_nHeight) << 3;
        {
            long lBitrate = m_Config.Bitrate;
            if (lBitrate <= 0 || (lBitrate < lRecomended && m_Config.bAutoBitrate))
            {
                lBitrate = lRecomended;
            }

            result = m_Encoder.SetProperty
                     (AMF.AMF_VIDEO_ENCODER_PROP.TARGET_BITRATE, lBitrate);
            lBitrate *= 10;
            result = m_Encoder.SetProperty
                     (AMF.AMF_VIDEO_ENCODER_PROP.PEAK_BITRATE, lBitrate);
        }
        int nCabac = (int)(m_Config.bCabac ? AMF.AMF_VIDEO_ENCODER_CODING.CABAC 
                                            : AMF.AMF_VIDEO_ENCODER_CODING.CALV);
        result = m_Encoder.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.CABAC_ENABLE, nCabac);
    }
    return S_OK;
}

我们使用 AMFComponentSetProperty() 方法(通过 AMFPropertyStorage 接口公开)来配置编码器设置。H264 组件有自己的参数,这些参数在 AMF_VIDEO_ENCODER_PROP 类中列为静态 string。对于任何其他组件,这些参数可能具有不同的名称或值,因此您应该查看 AMF SDK 中这些组件的文档。

最后一步是使用实际的输入设置(视频分辨率和表面格式)调用初始化编码器。如果所有先前的操作都已正确完成,则此方法将成功。如果在对象创建或编码器初始化期间发生故障,我们将调用 CloseEncoder() 方法,该方法将清除所有 AMF 资源。

// Free Encoder Objects
protected HRESULT CloseEncoder()
{
    lock (this) {
        if (m_Encoder != null)
        {
            m_Encoder.Terminate();
            m_Encoder.Dispose();
            m_Encoder = null;
        }
        if (m_Context != null)
        {
            m_Context.Terminate();
            m_Context.Dispose();
            m_Context = null;
        }
    }
    return NOERROR;
}

除了 Dispose(),我们还调用 AMFComponentAMFContext 对象的 Terminate() 方法,以清除这些对象持有的所有内部资源。调用 Terminate 方法后,访问 AMFComponent 可能会导致崩溃。

DirectShow 实现

编码器滤镜使用 TransformFilter 作为实现基类。我们需要重写它的抽象方法。

为了验证支持的输入格式,需要重写 CheckInputType() 方法。对于所有支持的输入,我们返回 S_OK。验证是基于 AMF SDK 列出的支持格式进行的。

// Validate input format
public override int CheckInputType(AMMediaType pmt)
{
    // We accept video only
    if (pmt.majorType != MediaType.Video)
    {
        return VFW_E_TYPE_NOT_ACCEPTED;
    }
    // Format must be specified
    if (pmt.formatType != FormatType.VideoInfo && pmt.formatType != FormatType.VideoInfo2)
    {
        return VFW_E_TYPE_NOT_ACCEPTED;
    }
    if (pmt.formatPtr == IntPtr.Zero)
    {
        return VFW_E_TYPE_NOT_ACCEPTED;
    }
    // Check the supported formats
    if (
            (pmt.subType != MediaSubType.YV12)
        && (pmt.subType != MediaSubType.UYVY)
        && (pmt.subType != MediaSubType.NV12)
        && (pmt.subType != MediaSubType.YUY2)
        && (pmt.subType != MediaSubType.YUYV)
        && (pmt.subType != MediaSubType.IYUV)
        && (pmt.subType != MediaSubType.RGB32)
        && (pmt.subType != MediaSubType.ARGB32)
        )
    {
        return VFW_E_TYPE_NOT_ACCEPTED;
    }
    // Check an alignment for the planar types
    if (
            (pmt.subType == MediaSubType.YV12)
        ||  (pmt.subType == MediaSubType.NV12)
        ||  (pmt.subType == MediaSubType.IYUV)
        )
    {
        BitmapInfoHeader _bmi = pmt;
        if (ALIGN16(_bmi.Width) != _bmi.Width)
        {
            return VFW_E_TYPE_NOT_ACCEPTED;
        }
    }
    return NOERROR;
}

CheckTransform() 方法也需要被重写,它用于验证输入和输出类型以进行转换。在编码器实现的情况下,我们只需返回 S_OK

我们应该从基本实现中重写的另一个方法是 SetMediaType()。它接收输入或输出引脚的最终媒体格式描述。在此方法中,我们准备所有关于输入格式和分辨率的信息,并使用这些设置初始化编码器组件。由于我们在 CPU 内存中接收数据,因此在提交给编码器组件之前,我们必须将其复制到 GPU 中。因此,我们还根据输入格式准备了每个平面的间距信息。

// Set input or output media format
public override int SetMediaType(PinDirection _direction, AMMediaType mt)
{
    HRESULT hr = (HRESULT)base.SetMediaType(_direction, mt);
    if (hr.Failed) return hr;
    // If we set input media type
    if (_direction == PinDirection.Input)
    {
        BitmapInfoHeader _bmi = mt;
        if (_bmi != null)
        {
            m_nWidth = _bmi.Width;
            m_nHeight = Math.Abs(_bmi.Height);
            m_bVerticalFlip = (_bmi.Height > 0);
        }
        // Configure settings of each format
        if (mt.subType == MediaSubType.YV12)
        {
            m_SurfaceFormat = AMF.AMF_SURFACE_FORMAT.YUV420P;
            m_hPitch = m_nWidth;
            m_vPitch = m_nHeight;
            m_bFlipUV = true;
        }
        if (mt.subType == MediaSubType.YUYV || mt.subType == MediaSubType.YUY2)
        {
            m_SurfaceFormat = AMF.AMF_SURFACE_FORMAT.YUY2;
            m_hPitch = m_nWidth << 1;
            m_vPitch = m_nHeight;
        }
        if (mt.subType == MediaSubType.NV12)
        {
            m_SurfaceFormat = AMF.AMF_SURFACE_FORMAT.NV12;
            m_hPitch = m_nWidth;
            m_vPitch = m_nHeight;
        }
        if (mt.subType == MediaSubType.UYVY)
        {
            m_SurfaceFormat = AMF.AMF_SURFACE_FORMAT.UYVY;
            m_hPitch = m_nWidth << 1;
            m_vPitch = m_nHeight;
        }
        if (mt.subType == MediaSubType.IYUV)
        {
            m_SurfaceFormat = AMF.AMF_SURFACE_FORMAT.YUV420P;
            m_hPitch = m_nWidth;
            m_vPitch = m_nHeight;
            m_bFlipUV = false;
        }
        if (mt.subType == MediaSubType.RGB32 || mt.subType == MediaSubType.ARGB32)
        {
            m_SurfaceFormat = AMF.AMF_SURFACE_FORMAT.ARGB;
            m_hPitch = m_nWidth << 2;
            m_vPitch = m_nHeight;
        }
        // Setup encoder instance
        hr = OpenEncoder();
        if (FAILED(hr)) return hr;
    }
    return hr;
}

要将输出引脚连接到下游滤镜,我们应该准备好支持的格式。这些格式是从滤镜请求的,并在重写的 GetMediaType() 方法中列出。参数中的索引是请求的输出媒体类型的基于零的索引。如果所有类型都已列出,方法应返回 VFW_S_NO_MORE_ITEMS,否则,媒体类型应填充在也作为参数传递的结构中。

// Build the output media types which are supported
public override int GetMediaType(int iPosition, ref AMMediaType pMediaType)
{
    if (pMediaType == null) return E_POINTER;
    if (iPosition < 0) return E_INVALIDARG;
    if (!Input.IsConnected) return VFW_E_NOT_CONNECTED;
    if (iPosition > 0) return VFW_S_NO_MORE_ITEMS;
    // Open encoder if it not yet done
    HRESULT hr = OpenEncoder();
    if (FAILED(hr)) return hr;

    pMediaType.majorType = MediaType.Video;
    pMediaType.subType = MEDIASUBTYPE_H264;

    BitmapInfoHeader _bmi = Input.CurrentMediaType;
    long _rate = 0;
    if (_rate == 0)
    {
        VideoInfoHeader vih = Input.CurrentMediaType;
        if (vih != null)
        {
            _rate = vih.AvgTimePerFrame;
        }
    }
    if (_rate == 0)
    {
        VideoInfoHeader2 vih = Input.CurrentMediaType;
        if (vih != null)
        {
            _rate = vih.AvgTimePerFrame;
        }
    }
    int _width = m_nWidth;
    int _height = m_nHeight;

    pMediaType.formatType = FormatType.VideoInfo;
    VideoInfoHeader _vih = new VideoInfoHeader();

    _vih.AvgTimePerFrame = _rate;

    _vih.BmiHeader.Size = Marshal.SizeOf(typeof(BitmapInfoHeader));
    _vih.BmiHeader.Width = _width;
    _vih.BmiHeader.Height = _height;
    _vih.BmiHeader.BitCount = 12;
    _vih.BmiHeader.ImageSize = _vih.BmiHeader.Width * Math.Abs(_vih.BmiHeader.Height) * 
                              (_vih.BmiHeader.BitCount > 0 ? _vih.BmiHeader.BitCount : 24) / 8;
    _vih.BmiHeader.Planes = 1;
    _vih.BmiHeader.Compression = MAKEFOURCC('H', '2', '6', '4');

    _vih.SrcRect.right = _width;
    _vih.SrcRect.bottom = _height;
    _vih.TargetRect.right = _width;
    _vih.TargetRect.bottom = _height;

    if (m_Config.Bitrate == 0)
    {
        _vih.BitRate = _vih.BmiHeader.ImageSize;
    }
    else
    {
        _vih.BitRate = (int)m_Config.Bitrate;
    }
    pMediaType.sampleSize = _vih.BmiHeader.ImageSize;

    pMediaType.SetFormat(_vih);

    return NOERROR;
}

我们将输出类型设置为 MEDIASUBTYPE_H264。格式参数,例如宽度、高度和帧率,是根据输入引脚连接格式设置的。
对于输出通信,我们应该指定分配器参数:缓冲区大小和缓冲区数量。这些设置在 DecideBufferSize() 重写方法中配置。

// Adjust output buffer size and number of buffers
public override int DecideBufferSize
       (ref IMemAllocatorImpl pAlloc, ref AllocatorProperties prop)
{
    if (!Output.IsConnected) return VFW_E_NOT_CONNECTED;
    AllocatorProperties _actual = new AllocatorProperties();
    BitmapInfoHeader _bmi = (BitmapInfoHeader)Input.CurrentMediaType;
    if (_bmi == null) return VFW_E_INVALIDMEDIATYPE;
    prop.cbBuffer = _bmi.GetBitmapSize();
    prop.cbAlign = 1;
    if (prop.cbBuffer < Input.CurrentMediaType.sampleSize)
    {
        prop.cbBuffer = Input.CurrentMediaType.sampleSize;
    }
    if (prop.cbBuffer < _bmi.ImageSize)
    {
        prop.cbBuffer = _bmi.ImageSize;
    }
    // Calculate optimal size for the output
    int lSize = (_bmi.Width * Math.Abs(_bmi.Height) * 
                (_bmi.BitCount + _bmi.BitCount % 8) / 8);
    if (prop.cbBuffer < lSize)
    {
        prop.cbBuffer = lSize;
    }
    // Number of buffers
    prop.cBuffers = 10;
    int hr = pAlloc.SetProperties(prop, _actual);
    return hr;
}

一旦我们开始处理图表,它就会调用 Pause() 方法,其状态等于 FilterState.Stopped。因此,在我们重写的 Pause() 实现中,我们准备初始输出参数,创建编码器(如果尚未初始化),并启动输出处理线程。

// Called once the filter switched into paused state
public override int Pause()
{
    // If we currently in the stopped state
    if (m_State == FilterState.Stopped)
    {
        m_evQuit.Reset();
        lock (this)
        {
            // In case if no encoder so far create it
            if (m_Encoder == null)
            {
                HRESULT hr = OpenEncoder();
                if (FAILED(hr)) return hr;
            }
        }
        // Initialize startup parameters
        m_bFirstSample = true;
        m_rtPosition = 0;
        // No input and output in queues
        m_evInput.Reset();
        m_evOutput.Reset();
        m_evFlush.Reset();
        // Start the encodig thread
        m_Thread.Create();
    }
    return base.Pause();
}

在图停止过程中,会调用 Stop() 方法。在该方法中,我们只是停止处理线程并进行垃圾回收。

// Called once the filter swithed into stopped state
public override int Stop()
{
    // Set quit and flush to exit all waiting threads
    m_evQuit.Set();
    m_evFlush.Set();
    // Shutdown encoder thread
    m_Thread.Close();
    // Switch into stopped state
    HRESULT hr = (HRESULT)base.Stop();
    GC.Collect();
    return hr;
}

还有两个方法可能需要重写。这些方法必须调用基类实现。一个是 BreakConnect() 方法。它表示输入或输出引脚已断开连接。在实现中,如果参数是输入引脚,我们释放编码器资源。

// Called once the pin disconnected
public override int BreakConnect(PinDirection _direction)
{
    HRESULT hr = (HRESULT)base.BreakConnect(_direction);
    if (hr.Failed) return hr;
    // If we disconnect input pin then we close the encoder
    if (_direction == PinDirection.Input)
    {
        CloseEncoder();
    }
    return hr;
}

我们需要重写的另一个方法是 CompleteConnect()。在这里,我们检查输入引脚是否已连接以及输出是否之前已连接的情况。如果连接了滤镜的两个引脚,然后决定重新连接输入,则可能会出现这种情况。在这种情况下,我们必须重新连接输出引脚,因为格式和缓冲区大小可能会随新的输入格式而改变。

// Final call for the connection completed
public override int CompleteConnect(PinDirection _direction, ref IPinImpl pPin)
{
    HRESULT hr = (HRESULT)base.CompleteConnect(_direction, ref pPin);
    if (hr.Failed) return hr;
    // Just reconnect output pin in case it was connected before the input
    if (_direction == PinDirection.Input && Output.IsConnected)
    {
        hr = (HRESULT)Output.ReconnectPin();
    }
    return hr;
}

接收样本

由于我们有一个编码器滤镜,我们可能在接收输入后不会立即获得输出数据。因此,我们需要在基类重写的 OnReceive() 方法中将数据传递给编码器,并将 Transform abstract 方法留空。

// Overriding abstract method, but it is not used here as we also override OnReceive
public override int Transform(ref IMediaSampleImpl pIn, ref IMediaSampleImpl pOut)
{
    return E_UNEXPECTED;
}

OnReceive() 方法的开头,我们应该处理输入媒体类型更改。当上层滤镜在输入引脚已连接后首次调用 CheckInputType() 方法(在引脚接口上调用 QueryAccept()),然后,如果我们可以接受新类型,则该类型被分配给传入的媒体样本时,这种情况是可能的。这种机制称为 QueryAccept (下游)

// Set the new media type it it set with the sample
{
    AMMediaType pmt;
    if (S_OK == _sample.GetMediaType(out pmt))
    {
        SetMediaType(PinDirection.Input,pmt);
        Input.CurrentMediaType.Set(pmt);
        pmt.Free();
    }
}

SetMediaType() 实现中,我们应该为格式可以动态更改的情况做好准备。因此,在流式传输状态和更改媒体类型时,我们检查分辨率或输入格式是否已更改,在这种情况下,重新创建编码器对象并重新启动输出线程。

// Store initial values to check encoder reset
int Width = m_nWidth;
int Height = m_nHeight;
AMF.AMF_SURFACE_FORMAT Format = m_SurfaceFormat;
bool bRunning = (m_State != FilterState.Stopped);
BitmapInfoHeader _bmi = mt;
if (_bmi != null)
{
    m_nWidth = _bmi.Width;
    m_nHeight = Math.Abs(_bmi.Height);
}
///...
// Type can be set while filter active 
if (bRunning)
{
    // In that case store actual video resolution
    VideoInfoHeader pvi = mt;
    if (pvi != null)
    {
        if (pvi.TargetRect.right - pvi.TargetRect.left < m_nWidth 
                    && pvi.TargetRect.right - pvi.TargetRect.left > 1)
        {
            m_nWidth = (pvi.TargetRect.right - pvi.TargetRect.left);
        }
        if (pvi.TargetRect.bottom - pvi.TargetRect.top < m_nHeight 
                    && pvi.TargetRect.bottom - pvi.TargetRect.top > 1)
        {
            m_nHeight = (pvi.TargetRect.bottom - pvi.TargetRect.top);
        }
    }
}
// Check if we need to reset encoder
if (m_nWidth != Width || m_nHeight != Height || Format != m_SurfaceFormat)
{
    // Shutdown encoder thread
    m_evQuit.Set();
    m_Thread.Close();
    // Close the encoder
    CloseEncoder();
}
///...
            
// If we running - starts the encoder thread
if (bRunning && !m_Thread.ThreadExists)
{
    m_bFirstSample = true;
    m_rtPosition = 0;
    m_evQuit.Reset();
    m_Thread.Create();
}

对于编码器输入,我们需要传递分配在 GPU 内存上的 AMFSurface 对象。该表面的格式应与我们用于初始化编码器组件的格式相同。一旦我们接收到大多数格式的视频数据,我们就可以创建封装的 AMFSurface 对象。这是针对实际媒体样本数据缓冲区完成的,无需任何复制。这种表面内存是主机或 CPU。为此,我们可以使用 AMFContext 对象的 CreateSurfaceFromHostNative() 方法。创建的表面可以复制到分配在 GPU 内存上的另一个 AMFSurface 对象。这可以通过 AMFSurface 接口的 CopySurfaceRegion() 方法完成。

IntPtr p;
_sample.GetPointer(out p);
AMF.AMFSurface output_surface = null;
AMF.AMFSurface surface = null;

AMF.AMF_RESULT result = AMF.AMF_RESULT.FAIL;

// Create otuput surface on GPU
result = m_Context.AllocSurface
         (m_DecoderType, m_SurfaceFormat, m_nWidth, m_nHeight, out output_surface);
if (result == AMF.AMF_RESULT.OK)
{
    // Wrap host surface with the media sample buffer
    result = m_Context.CreateSurfaceFromHostNative(m_SurfaceFormat, m_nWidth, 
                                                   m_nHeight, m_hPitch, 
                                                   m_vPitch, p, out surface);
}

// If we have wrapped surface
if (result == AMF.AMF_RESULT.OK && surface != null)
{
    // Copy it to the GPU memory 
    result = surface.CopySurfaceRegion(output_surface, 0, 0, 0, 0, m_nWidth, m_nHeight);
}

对于 RGB 数据,图像数据可能以倒置或垂直翻转的方式到达。在媒体管道中,如果媒体类型中高度为正值,则 RGB 数据以倒置方式到达;否则,高度为负值,表示图像数据以常规方式或自上而下的方式到达。

在图像倒置的情况下,我们无法使用封装的缓冲区表面,因为那样我们也会得到垂直翻转的视频。为此,我们在主机或 CPU 上使用 AMFContext 接口的 AllocSurface() 方法创建一个 AMFSurface 对象,并将图像数据复制到该表面,同时将图像恢复为倒置顺序。之后,我们调用 AMFSurfaceConvert() 方法并指定所需的平台类型:在我们的例子中是 DirectX11。

if (m_SurfaceFormat == AMF.AMF_SURFACE_FORMAT.ARGB && m_bVerticalFlip)
{
    result = m_Context.AllocSurface(AMF.AMF_MEMORY_TYPE.HOST, m_SurfaceFormat, m_nWidth, 
                                    m_nHeight, out output_surface);
    if (result == AMF.AMF_RESULT.OK)
    {
        p = (IntPtr)(p.ToInt64() + m_hPitch * (m_nHeight - 1));
        var plane = output_surface.GetPlane(AMF.AMF_PLANE_TYPE.PACKED);
        var dest = plane.Native;
        for (int i = 0; i < m_nHeight; i++)
        {
            API.CopyMemory(dest,p, m_hPitch);
            dest = (IntPtr)(dest.ToInt64() + plane.HPitch);
            p = (IntPtr)(p.ToInt64() - m_hPitch);
        }
        // Convert surface to the GPU memory type
        result = output_surface.Convert(m_DecoderType);
    }
}

对于 YV12 输入格式,我们还需要在主机上创建一个表面对象,然后将其转换为 GPU 内存。这是必要的,因为 AMF SDK 不支持 YV12 格式,但它支持 IYUV,即 AMF SDK 中的 YUV420P 格式。这些格式是相似的 420 平面类型,具有三个平面,但只有一个区别:UV 平面替换。因此,我们需要分配 IYUV (YUV420P) AMFSurface 并翻转 U 和 V 复制源数据的每个平面。然后,也调用 Convert() 表面方法以将其导入 GPU。

// For YV12 format we have to do input manually as YV12 format not supported
if (m_SurfaceFormat == AMF.AMF_SURFACE_FORMAT.YUV420P && m_bFlipUV)
{
    // Allocate YUV420P Surface on HOST
    result = m_Context.AllocSurface(AMF.AMF_MEMORY_TYPE.HOST, m_SurfaceFormat, m_nWidth, 
                                    m_nHeight, out output_surface);
    if (result == AMF.AMF_RESULT.OK)
    {
        // Setup source plane pointers
        IntPtr y = p;
        IntPtr v = (IntPtr)(p.ToInt64() + m_hPitch * m_vPitch);
        IntPtr u = (IntPtr)(v.ToInt64() + ((m_hPitch * m_vPitch) >> 2));
        int hPitch_d2 = m_hPitch >> 1;
        int nWidth_d2 = m_nWidth >> 1;

        // Setup destination plane pointers
        int y_stride = output_surface.GetPlane(AMF.AMF_PLANE_TYPE.Y).HPitch;
        int v_stride = output_surface.GetPlane(AMF.AMF_PLANE_TYPE.V).HPitch;
        var dY = output_surface.GetPlane(AMF.AMF_PLANE_TYPE.Y).Native;
        var dV = output_surface.GetPlane(AMF.AMF_PLANE_TYPE.V).Native;
        var dU = output_surface.GetPlane(AMF.AMF_PLANE_TYPE.U).Native;
                                
        // Copy planes data
        for (int i = 0; i < (m_nHeight >> 1); i++)
        {
            API.CopyMemory(dY, y, m_nWidth);
            y = (IntPtr)(y.ToInt64() + m_hPitch);
            dY = (IntPtr)(dY.ToInt64() + y_stride);
            API.CopyMemory(dY, y, m_nWidth);
            y = (IntPtr)(y.ToInt64() + m_hPitch);
            dY = (IntPtr)(dY.ToInt64() + y_stride);
            API.CopyMemory(dV, v, nWidth_d2);
            v = (IntPtr)(v.ToInt64() + hPitch_d2);
            dV = (IntPtr)(dV.ToInt64() + v_stride);
            API.CopyMemory(dU, u, nWidth_d2);
            u = (IntPtr)(u.ToInt64() + hPitch_d2);
            dU = (IntPtr)(dU.ToInt64() + v_stride);
        }
        // Convert surface to the GPU memory type
        result = output_surface.Convert(m_DecoderType);
    }
}

一旦输出表面准备就绪,我们应该指定可以从传入媒体样本中获取的 timestampduration

// Configure timings and set it to the output surface 
long start, stop;
hr = (HRESULT)_sample.GetTime(out start, out stop);

if (hr >= 0)
{
    output_surface.pts = start;
    if (hr == 0)
    {
        output_surface.Duration = stop - start;
        m_rtPosition = stop;
    }
    else
    {
        output_surface.Duration = m_rtFrameRate;
        m_rtPosition = start + m_rtFrameRate;
    }
}
else
{
    output_surface.pts = m_rtPosition;
    output_surface.Duration = m_rtFrameRate;
    m_rtPosition += m_rtFrameRate;
}

对于第一个样本,需要编码器初始化信息,因此我们应该为编码器设置一个标志,以将 sps pps 数据插入到比特流中。此外,我们应该发出信号,表明第一个帧将是关键帧。

// For the first sample setup SPS/PPS and force IDR frame
if (m_bFirstSample)
{
    output_surface.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.FORCE_PICTURE_TYPE, 
                                (int)AMF.AMF_VIDEO_ENCODER_PICTURE_TYPE.IDR);
    output_surface.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.INSERT_SPS, true);
    output_surface.SetProperty(AMF.AMF_VIDEO_ENCODER_PROP.INSERT_PPS, true);
    m_bFirstSample = false;
}

准备好后,我们将表面提交给编码器。这通过调用 AMFComponent 对象的 SubmitInput() 方法完成。但由于输入表面快速到来且编码器内部输入队列已满,提交可能会失败。在这种情况下,我们将等待输出事件被发出信号。当输出比特流数据准备就绪时,此事件会设置,因此我们可以尝试向编码器提供新的输入。因此,我们开始在循环中等待此类事件通知或退出或刷新信号。一旦我们成功地向编码器提供数据,我们也会通过事件发出信号,表示已提交新的输入表面进行编码。

hr = S_OK;
do
{
    lock (this)
    {
        // Submit Surface to the Encoder
        if (m_Encoder != null)
        {
            result = m_Encoder.SubmitInput(output_surface);
        }
        else
        {
            hr = E_FAIL;
            break;
        }
    }
    // If we succeeded signal to the output thread
    if (result == AMF.AMF_RESULT.OK)
    {
        m_evInput.Set();
        break;
    }
    else
    {
        // Otherwise wait for the encoder free slot
        if (0 != WaitHandle.WaitAny(new WaitHandle[] { m_evOutput, m_evFlush, m_evQuit }))
        {
            break;
        }
    }
} while (hr == S_OK);

交付输出样本

AMF 组件的主要目的是处理媒体流,通常作为管道的一部分。AMFComponent 接收 AMFData 对象作为输入,也提供 AMFData 作为输出。在编码器实现中,我们有 AMFSurface,其基类是输入上的 AMFData,输出上是 AMFBuffer,后者也派生自 AMFData
输出交付在单独的线程中执行。一旦滤镜切换到活动状态,此线程就会启动。该线程设置本地编码器对象并增加其引用。在处理循环中,我们从编码器请求输出数据,如果数据尚未可用,则切换到等待状态。我们等待三个事件之一:输入信号(在新表面传递到编码器后设置)、退出信号(设置为关闭)和刷新通知。

while (true)
{
    AMF.AMFData data;
    // Request for the output bitstream data 
    var result = encoder.QueryOutput(out data);
    if (data != null)
    {
        ///...
        data.Dispose();
    }
    else
    {
        // if flushing, we can get EOF exit the loop in that case
        if (result != AMF.AMF_RESULT.OK && flushing)
        {
            encoder.Flush();
            break;
        }
        // If no flushing signals
        if (!flushing)
        {
            // We are waiting for input or quit
            int wait = WaitHandle.WaitAny(new WaitHandle[] 
                       { m_evInput, m_evFlush, m_evQuit });
            if (0 != wait)
            {
                // If quit, then exit the loop
                if (m_evQuit.WaitOne(0))
                {
                    break;
                }
                else
                {
                    // signal to discard input samples
                    flushing = true;
                    encoder.Drain();
                }
            }
        }
    }
}

如果新的输出数据可用并且它支持 AMFBuffer 接口,那么该缓冲区中的数据将被复制到 IMediaSample 接口中。AMFBuffer 还包含编码视频帧的 timestampduration,这些也设置到 IMediaSample 中。从缓冲区对象的请求属性 AMF_VIDEO_ENCODER_PROP.OUTPUT_DATA_TYPE,我们可以为输出数据设置关键帧标志。

// Query for the AMFBuffer interface
var buffer = data.QueryInterface<AMF.AMFBuffer>();
if (buffer != null)
{
    // Get free sample from the output allocator
    IntPtr pSample = IntPtr.Zero;
    HRESULT hr = (HRESULT)Output.GetDeliveryBuffer(out pSample, null, null, AMGBF.None);
    if (hr == S_OK)
    {
        IMediaSampleImpl sample = new IMediaSampleImpl(pSample);
        IntPtr p;
        // Copy media data
        sample.GetPointer(out p);
        sample.SetActualDataLength(buffer.Size);
        API.CopyMemory(p, buffer.Native, buffer.Size);
        // Setting up sample timings
        long start = buffer.pts;
        long stop = buffer.pts + buffer.Duration;
        sample.SetTime(start, stop);
        int type = 0;
        // An IDR frame should be marked as sync point
        if (AMF.AMF_RESULT.OK == 
                buffer.GetProperty(AMF.AMF_VIDEO_ENCODER_PROP.OUTPUT_DATA_TYPE, out type))
        {
            sample.SetSyncPoint((int)AMF.AMF_VIDEO_ENCODER_OUTPUT_DATA_TYPE.IDR == type);
        }
        ///...
    }
    buffer.Dispose();
}

为了在从输出引脚分配器请求 IMediaSample 后立即支持动态格式更改,我们必须验证它是否包含新的媒体类型信息。由于我们只支持 AVC 或 H264,因此我们只会在此部分有所不同,因此在我们的情况下,无需对该类型执行额外的检查,我们只需将其作为参数传递给 SetMediaType() 并指定输出方向。

// Takes care if the upper filter change type - for example switched from annex b to avc
{
    AMMediaType pmt;
    if (S_OK == sample.GetMediaType(out pmt))
    {
        SetMediaType(PinDirection.Output, pmt);
        Output.CurrentMediaType.Set(pmt);
        pmt.Free();
    }
}

通过这种方式,我们处理了一种称为 QueryAccept (上游) 的动态格式更改机制。
一旦 IMediaSample 准备就绪,它就会通过调用输出引脚的 Deliver() 方法(以媒体样本作为参数)将其传递给下游滤镜。之后,我们设置一个输出事件,这样我们就表示编码器可以接受新的输入数据。

// Deliver sample
Output.Deliver(ref sample);
sample._Release();
// Signal for the free input slot
m_evOutput.Set();

输出数据的处理将一直进行,直到 quitflush 事件设置为 signaled 状态。在这些事件上,线程退出。

用于调试的流转储

在交付输出期间,出于测试目的,我们可能需要保存编码器组件生成的数据。为此,如果存在调试转储文件名,我们将创建一个输出流。该流在输出线程启动期间创建,文件名为 m_sDumpFileName 类变量中指定的文件名。我们还会初始化一个辅助字节数组。

// Prepare debug dump stream
Stream stream = null;
byte[] output = new byte[1920 * 1080];
if (!string.IsNullOrEmpty(m_sDumpFileName))
{
    try
    {
        stream = new FileStream(m_sDumpFileName, FileMode.Create, 
                                FileAccess.Write, FileShare.Read);
    }
    catch
    {
    }
}

在输出交付循环中,一旦我们得到 AMFBuffer 并且有输出流,我们将接收到的数据复制到准备好的数组并保存到流中。如果数组中没有足够的空间容纳数据,那么我们只需重新调整它的大小。

// Debug dump if initialized
if (stream != null)
{
    if (buffer.Size > output.Length)
    {
        Array.Resize<byte>(ref output, buffer.Size);
    }
    Marshal.Copy(buffer.Native, output, 0, buffer.Size);
    stream.Write(output, 0, buffer.Size);
}

在退出输出循环时,如果创建了输出流,我们也会将其释放。

if (stream != null)
{
    stream.Dispose();
}

每次执行文件查找操作时(在流结束通知时也会调用),转储文件都将被重新创建。
您可以看到二进制数据是带有起始码的 H264 比特流格式。

并且该二进制文件可以使用 GraphEdit 工具播放。

AVC 和 Annex B

H264的媒体数据可以以完整帧的形式交付,并带有如ITU-T H.264 标准文档附录b所述的起始码前缀比特流格式。为了表示我们正在提供AVC帧,我们以MPEG2VIDEO格式准备输出媒体类型,并指定解码器初始化额外数据参数和帧长度大小前缀。我们将帧长度设置为四字节长。我们通过请求编码器组件的AMF_VIDEO_ENCODER_PROP.PROFILEAMF_VIDEO_ENCODER_PROP.PROFILE_LEVEL属性来获取配置文件和级别。

AMF.AMFInterface data;
AMF.AMF_RESULT result;
lock (this)
{
    if (m_Encoder == null) return VFW_S_NO_MORE_ITEMS;
    // Request SPS PPS extradata
    result = m_Encoder.GetProperty(AMF.AMF_VIDEO_ENCODER_PROP.EXTRADATA, out data);
    // If no info - just skip that type
    if (data == null) return VFW_S_NO_MORE_ITEMS;
    // Request profile and level
    result = m_Encoder.GetProperty(AMF.AMF_VIDEO_ENCODER_PROP.PROFILE, out _profile_idc);
    result = m_Encoder.GetProperty
             (AMF.AMF_VIDEO_ENCODER_PROP.PROFILE_LEVEL, out _level_idc);
}

我们从编码器获取初始化参数作为 AMF_VIDEO_ENCODER_PROP.EXTRADATA 属性。数据采用起始码前缀格式,因此我们应该从中提取 spspps 列表。

List<byte[]> spspps = new List<byte[]>();

var buffer = data.QueryInterface<AMF.AMFBuffer>();
data.Dispose();
if (buffer != null)
{
    IntPtr p = buffer.Native;
    IntPtr nalu = IntPtr.Zero;
    int total = buffer.Size;
    int offset = 0;
    // Split up the sps pps nalu
    while (total > 3)
    {
        if (0x01000000 == Marshal.ReadInt32(p, offset))
        {
            if (nalu != IntPtr.Zero)
            {
                int type = (Marshal.ReadByte(nalu) & 0x1f);
                if (type == 7 || type == 8) // sps pps
                {
                    byte[] info = new byte[offset];
                    Marshal.Copy(nalu, info, 0, offset);
                    spspps.Add(info);
                }
            }
            nalu = (IntPtr)(p.ToInt64() + offset + 4);
            total -= 4;
            p = nalu;
            offset = 0;
        }
        else
        {
            offset++;
            total--;
        }
    }
    if (nalu != IntPtr.Zero)
    {
        int type = (Marshal.ReadByte(nalu) & 0x1f);
        offset += total;
        if (type == 7 || type == 8) // sps pps
        {
            byte[] info = new byte[offset];
            Marshal.Copy(nalu, info, 0, offset);
            spspps.Add(info);
        }
    }
}

在将初始化数据设置为格式之前,我们应该准备额外数据格式。在 MPEG2VIDEO 中,前两个字节指定额外数据的大小,其后是实际数据。

// If we have SPS PPS
if (spspps.Count > 0)
{
    // Allocate extra data buffer
    pExtraData = Marshal.AllocCoTaskMem(100 + buffer.Size);
    if (pExtraData != IntPtr.Zero)
    {
        IntPtr p = pExtraData;
        // Fill up the buffer
        for (int i = 0; i < spspps.Count; i++)
        {
            Marshal.WriteByte(p, 0, (byte)((spspps[i].Length >> 8) & 0xff));
            Marshal.WriteByte(p, 1, (byte)(spspps[i].Length & 0xff));
            p = (IntPtr)(p.ToInt64() + 2);
            Marshal.Copy(spspps[i], 0, p, spspps[i].Length);
            p = (IntPtr)(p.ToInt64() + spspps[i].Length);
        }
        // Ammont of actual data
        _extrasize = (int)(p.ToInt64() - pExtraData.ToInt64());
        pMediaType.subType = MEDIASUBTYPE_AVC;
    }
}

在本例中,MPEG2VIDEO 的完整格式初始化如下。

if (_extrasize > 0) {
    _vih.BmiHeader.Compression = MAKEFOURCC('A', 'V', 'C', '1');
}
_mpegVI.dwProfile = (uint)_profile_idc;
_mpegVI.dwLevel = (uint)_level_idc;
_mpegVI.dwFlags = 4;
if (pExtraData != IntPtr.Zero && _extrasize > 0)
{
    _mpegVI.cbSequenceHeader = (uint)_extrasize;
    _mpegVI.dwSequenceHeader = new byte[_extrasize];
    Marshal.Copy(pExtraData, _mpegVI.dwSequenceHeader, 0, _extrasize);
}
pMediaType.formatSize = Marshal.SizeOf(_mpegVI) + _extrasize;
pMediaType.formatPtr = Marshal.AllocCoTaskMem(pMediaType.formatSize);
Marshal.StructureToPtr(_mpegVI, pMediaType.formatPtr, false);
if (_mpegVI.dwSequenceHeader != null && _mpegVI.dwSequenceHeader.Length > 0)
{
    int offset = Marshal.OffsetOf(_mpegVI.GetType(), "dwSequenceHeader").ToInt32();
    Marshal.Copy(_mpegVI.dwSequenceHeader, 0, 
                    new IntPtr(pMediaType.formatPtr.ToInt64() + offset), 
                    _mpegVI.dwSequenceHeader.Length);
}

此外,该格式的子类型设置为 MEDIASUBTYPE_AVC。在媒体类型协商过程中,我们检查该值以确定数据传递方式。

// If we set output media type
if (_direction == PinDirection.Output)
{
    hr = OpenEncoder();
    // Check for the AVC output
    m_bAVC = (mt.subType == MEDIASUBTYPE_AVC);
}

我们从编码器输出获得的数据始终是比特流格式:带有起始码前缀。因此,我们应该手动处理 AVC 输出请求,并在输出类型为 AVC 时用数据大小替换起始码。这样做很好,因为我们将媒体类型中的前缀大小长度设置为四个字节,这等于起始码长度。在输出循环的实现中,当我们初始化输出样本数据时,我们检查下游滤镜是否需要 AVC 输入。在这种情况下,我们查找起始码前缀,并以此方式计算前一个 nalu 的大小。用该值大小替换起始码前缀。

// Setup length information in case we output non annex b
if (m_bAVC)
{
    IntPtr nalu = IntPtr.Zero;
    int total = buffer.Size;
    int offset = 0;
    while (total > 3)
    {
        if (0x01000000 == Marshal.ReadInt32(p, offset))
        {
            if (nalu != IntPtr.Zero)
            {
                Marshal.WriteByte(nalu, 0, (byte)((offset >> 24) & 0xff));
                Marshal.WriteByte(nalu, 1, (byte)((offset >> 16) & 0xff));
                Marshal.WriteByte(nalu, 2, (byte)((offset >> 8) & 0xff));
                Marshal.WriteByte(nalu, 3, (byte)((offset >> 0) & 0xff));
            }
            nalu = (IntPtr)(p.ToInt64() + offset);
            total -= 4;
            p = (IntPtr)(nalu.ToInt64() + 4);
            offset = 0;
        }
        else
        {
            offset++;
            total--;
        }
    }
    if (nalu != IntPtr.Zero)
    {
        offset += total;
        Marshal.WriteByte(nalu, 0, (byte)((offset >> 24) & 0xff));
        Marshal.WriteByte(nalu, 1, (byte)((offset >> 16) & 0xff));
        Marshal.WriteByte(nalu, 2, (byte)((offset >> 8) & 0xff));
        Marshal.WriteByte(nalu, 3, (byte)((offset >> 0) & 0xff));
    }
}

刷新

查找文件或收到 EOS 后,需要从编码器中排出所有未完成的数据并将其传递到输出。

在编码器输出线程中,我们等待退出事件或刷新事件信号。在 flush 事件上,我们调用编码器组件的 Drain() 方法。一旦编码器接收到它,它就会丢弃任何输入数据,直到调用 Flush() 方法。此时,我们请求编码器中的所有挂起缓冲区,并将它们传递到输出引脚。一旦输出缓冲区为空,我们调用 Flush() 方法,编码器即可用于新的输入数据。

// if flushing we can get EOF exit the loop in that case
if (result != AMF.AMF_RESULT.OK && flushing)
{
    encoder.Flush();
    break;
}
// If no flushing signals
if (!flushing)
{
    // We are waiting for input or quit
    int wait = WaitHandle.WaitAny(new WaitHandle[] { m_evInput, m_evFlush, m_evQuit });
    if (0 != wait)
    {
        // If quit then exit the loop
        if (m_evQuit.WaitOne(0))
        {
            break;
        }
        else
        {
            // signal to discard input samples
            flushing = true;
            encoder.Drain();
        }
    }
}

DirectShow 滤镜中,当刷新开始时,我们收到 BeginFlush() 方法调用。在重写的方法中,我们发出刷新开始的信号。

// Start draining samples from the encoder
public override int BeginFlush()
{
    HRESULT hr = (HRESULT)base.BeginFlush();
    if (hr.Failed) return hr;
    // Signal that we are flushing
    m_evFlush.Set();
    return hr;
}

当刷新操作完成时,调用 EndFlush() 基类 DirectShow 方法。在我们的实现中,我们重新启动输出线程并重置启动设置。

// End flushing 
public override int EndFlush()
{
    // Stop endcoding thread
    if (!m_Thread.Join(1000))
    {
        m_evQuit.Set();
    }
    // Reset startup settings
    m_bFirstSample = true;
    m_rtPosition = 0;
    // Reset events
    m_evFlush.Reset();
    m_evQuit.Reset();
    // Start encoding thread
    m_Thread.Create();
    return base.EndFlush();
}

与应用程序通信

为了与应用程序通信,创建接口 IH264Encoder,可以从滤镜请求。这是一个常规的 .NET 接口,但它被导出到滤镜类型库中,并且可以通过 COM 访问,因为我们为该接口声明指定了 GuidComVisible 属性。

[ComVisible(true)]
[System.Security.SuppressUnmanagedCodeSecurity]
[Guid("825AE8F7-F289-4A9D-8AE5-A7C97D518D8A")]
[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_Preset([Out] out quality_preset pValue);

    [PreserveSig]
    int put_Preset([In] quality_preset 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);
}

编码设置可以通过该接口配置。大多数设置在滤镜处于活动状态时无法修改。在这种情况下,它会返回 VFW_E_NOT_STOPPED

如果滤镜的输出引脚已连接且连接类型为 AVC,则在更改属性时将重新连接输出引脚。因为 AVC 编码器配置是在媒体类型中指定的。

可以通过接口配置的设置是基本的编码器设置。如何将它们设置到编码器组件中,您可以查看前面讨论的 ConfigureEncoder() 方法。您可以准备自己的配置并设置任何属性,即使此处未指定。为此,请遵循 AMF SDK 中指定的组件文档。
滤镜不支持 IPersistStreamIPersistPropertyBag 接口。

滤镜属性页

编码器设置可以通过属性页对话框进行配置,该对话框通过公开的 IH264Encoder 接口处理设置。

在属性页上,可以使用基本设置配置编码器。每个滤镜实例的设置都会重置,但您可以轻松添加将它们加载和保存到系统注册表的功能。

滤镜概述

滤镜具有良好的编码性能。

DirectShow 滤镜在视频压缩器类别中注册。

创建的 DirectShow 滤镜支持使用 AMF SDK 在 AMD 显卡上进行 H264 视频编码。编码的输入格式为 NV12YV12IYUVUYVYYUY2YUYVRGB32ARGB。对于平面输入类型(NV12YV12IYUV),宽度需要有 16 位的对齐。YV12 格式通过翻转 UV 平面(在 CPU 上执行)作为 IYUV 完成。RGB 类型支持在 CPU 上执行的垂直翻转。

滤镜使用 DirectX11 平台进行上下文初始化。要创建任何其他平台上下文,您应该查看 AMF SDK 文档

滤镜具有三种输出媒体类型,可以提供比特流和 AVC 格式的数据。

编码后的视频甚至可以通过 AVI Mux 滤镜保存到文件中。播放效果也很好。

二进制文件应使用 RegAsm 工具注册为 COM 类型库,该工具位于“*WINDIR\Microsoft.NET\Framework\XXXX*”中,其中 XXXX 是平台版本。如果您从源代码构建二进制文件,它会自动注册 COM 互操作,因为在项目设置中已启用该功能。

历史

  • 2024年1月24日:初始版本
© . All rights reserved.