我在这里遇到了一些潜在的问题。罗曼提到了两个大问题,所以我将详细说明它们。我还有一些其他的批评/建议给你。
不使用IMFDXGIDeviceManager
为了在 Media Foundation 中使用硬件加速,您需要创建一个 DirectX 设备管理器对象,可以是IDirect3DDeviceManager9对于 DX9 或您的情况IMFDXGIDeviceManager对于 DXGI。我强烈建议阅读该接口的所有 MSDN 文档。之所以有必要,是因为同一个 DX 设备必须在所使用的所有协作硬件 MF 转换之间共享,因为它们都需要访问设备控制的共享 GPU 内存,并且每个设备在工作时都需要对设备进行独占控制,因此需要一个锁定系统。设备管理器对象提供了该锁定系统,也是为一个或多个转换提供 DX 设备的标准方式。对于 DXGI,您可以使用以下命令创建它MFCreateDXGIDeviceManager.
从那里,您需要创建 DX11 设备,并调用IMFDXGIDeviceManager::ResetDevice与您的 DX11 设备。然后,您需要为 Sink Writer 本身设置设备管理器,这在您上面提供的代码中没有完成。这是这样完成的:
// ... inside your InitializeSinkWriter function that you listed above
// I'm assuming you've already created and set up the DXGI device manager elsewhere
IMFDXGIDeviceManager pDeviceManager;
// Passing 3 as the argument because we're adding 3 attributes immediately, saves re-allocations
if (Succeeded(hr)) hr = (int)MFExtern.MFCreateAttributes(out attributes, 3);
if (Succeeded(hr)) hr = (int)attributes.SetUINT32(MFAttributesClsid.MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, 1);
if (Succeeded(hr)) hr = (int)attributes.SetUINT32(MFAttributesClsid.MF_LOW_LATENCY, 1);
// Here's the key piece!
if (Succeeded(hr)) hr = (int)attributes.SetUnknown(MFAttributesClsid.MF_SINK_WRITER_D3D_MANAGER, pDeviceManager);
// Create the sink writer
if (Succeeded(hr)) hr = (int)MFExtern.MFCreateSinkWriterFromURL(outputFile, null, attributes, out sinkWriter);
这实际上将启用 D3D11 对硬件编码器的支持,并允许其访问读取Texture2D
你正在路过。值得注意的是MF_SINK_WRITER_D3D_MANAGER
适用于 DX9 和 DXGI 设备管理器。
编码器缓冲多个IMFSample
相同纹理的实例
这也是问题的潜在原因 - 至少它会导致很多意外行为,即使它不是明显问题的原因。根据 Roman 的评论,许多编码器将缓冲多个帧作为其编码过程的一部分。使用 Sink Writer 时您不会看到这种行为,因为它会为您处理所有细节工作。然而,您想要完成的任务(即发送 D3D11 纹理作为输入帧)的级别足够低,您开始不得不担心 Sink Writer 使用的编码器 MFT 的内部细节。
大多数视频编码器 MFT 将使用一定大小的内部缓冲区来存储最后一个N样品通过提供IMFTransform::ProcessInput。这具有副作用,即在生成任何输出之前必须提供多个样本作为输入。视频编码器需要按顺序访问多个样本,因为它们使用后续帧来确定如何对当前帧进行编码。换句话说,如果解码器正在处理帧 0,它可能还需要查看帧 1、2 和 3。从技术角度来看,这是因为帧间预测和运动估计。一旦编码器完成处理最旧的样本,它就会生成一个输出缓冲区(另一个IMFSample
对象,但这次在输出端通过IMFTransform::ProcessOutput)然后丢弃它正在处理的输入样本(通过调用IUnknown::Release
),然后请求更多输入,并最终进入下一帧。您可以在 MSDN 文章中阅读有关此过程的更多信息在编码器中处理数据
正如 Roman 所提到的,这意味着你正在封装一个ID3D11Texture2D
里面一个IMFMediaBuffer
里面一个IMFSample
然后将其传递给 Sink Writer。作为编码过程的一部分,该样本可能由编码器缓冲。当编码器工作时,该内容Texture2D
可能正在发生变化,这可能会导致各种问题。即使这不会导致程序错误,它也肯定会导致非常奇怪的编码视频输出。想象一下,如果编码器试图预测一帧的视觉内容在下一帧中如何变化,然后两帧的实际视觉内容从编码器下更新!
发生这个特定问题是因为编码器只有一个指向您的指针引用IMFSample
实例,它最终只是一个指向你的实例的指针ID3D11Texture2D
对象,该对象是一种对可变图形内存的指针引用。最终,该图形内存的内容会由于程序的其他部分而发生变化,但由于更新的始终是相同的 GPU 纹理,因此您发送给编码器的每个样本都指向相同的单个纹理。这意味着每当您通过更改 GPU 内存来更新纹理时,所有活动的IMFSample
对象将反映这些变化,因为它们都有效地指向相同的 GPU 纹理。
要解决此问题,您需要分配多个ID3D11Texture2D
对象,这样您就可以将一个纹理与一个纹理配对IMFSample
将其提供给 Sink Writer 时。这将通过使每个样本指向唯一的纹理来解决所有样本都指向同一个 GPU 纹理的问题。不过,您不一定知道需要创建多少纹理,因此处理此问题的最安全方法是编写自己的纹理分配器。这仍然可以在 C# 中完成,因为 MediaFoundation.NET 已经定义了您需要使用的接口。
分配器应该维护一个“空闲”列表SharpDX.Texture2D
对象 - 那些当前未被接收器写入器/编码器使用的对象。您的程序应该能够从分配器请求新的纹理对象,在这种情况下,它将从空闲列表中返回一个对象,或者创建一个新的纹理来满足该请求。
下一个问题是知道什么时候IMFSample
对象已被编码器丢弃,因此您可以将附加的纹理添加回空闲列表。碰巧的是,MFCreateVideoSampleFromSurface
您当前使用的函数分配实现以下功能的样本IMFTrackedSample界面。您将需要该界面,以便在释放样本时收到通知,以便您可以回收Texture2D
对象。
诀窍是你必须告诉样本:you是分配器。首先,您的分配器类需要实现IMFAsyncCallback。如果您通过以下方式在示例上设置分配器类IMFTrackedSample::SetAllocator,你的分配器的IMFAsyncCallback::Invoke方法将被调用,带有IMFAsyncResult每当编码器释放样本时作为参数传递。下面是分配器类的一般示例。
sealed class TextureAllocator : IMFAsyncCallback, IDisposable
{
private ConcurrentStack<SharpDX.Direct3D11.Texture2D> m_freeStack;
private static readonly Guid s_IID_ID3D11Texture2D = new Guid("6f15aaf2-d208-4e89-9ab4-489535d34f9c");
// If all textures are the exact same size and color format,
// consider making those parameters private class members and
// requiring they be specified as arguments to the constructor.
public TextureAllocator()
{
m_freeStack = new ConcurrentStack<SharpDX.Direct3D11.Texture2D>();
}
private bool disposedValue = false;
private void Dispose(bool disposing)
{
if(!disposedValue)
{
if(disposing)
{
// Dispose managed resources here
}
if(m_freeStack != null)
{
SharpDX.Direct3D11.Texture2D texture;
while(m_freeStack.TryPop(out texture))
{
texture.Dispose();
}
m_freeStack = null;
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~TextureAllocator()
{
Dispose(false);
}
private SharpDX.Direct3D11.Texture2D InternalAllocateNewTexture()
{
// Allocate a new texture with your format, size, etc here.
}
public SharpDX.Direct3D11.Texture2D AllocateTexture()
{
SharpDX.Direct3D11.Texture2D existingTexture;
if(m_freeStack.TryPop(out existingTexture))
{
return existingTexture;
}
else
{
return InternalAllocateNewTexture();
}
}
public IMFSample CreateSampleAndAllocateTexture()
{
IMFSample pSample;
IMFTrackedSample pTrackedSample;
HResult hr;
// Create the video sample. This function returns an IMFTrackedSample per MSDN
hr = MFExtern.MFCreateVideoSampleFromSurface(null, out pSample);
MFError.ThrowExceptionForHR(hr);
// Query the IMFSample to see if it implements IMFTrackedSample
pTrackedSample = pSample as IMFTrackedSample;
if(pTrackedSample == null)
{
// Throw an exception if we didn't get an IMFTrackedSample
// but this shouldn't happen in practice.
throw new InvalidCastException("MFCreateVideoSampleFromSurface returned a sample that did not implement IMFTrackedSample");
}
// Use our own class to allocate a texture
SharpDX.Direct3D11.Texture2D availableTexture = AllocateTexture();
// Convert the texture's native ID3D11Texture2D pointer into
// an IUnknown (represented as as System.Object)
object texNativeObject = Marshal.GetObjectForIUnknown(availableTexture.NativePointer);
// Create the media buffer from the texture
IMFMediaBuffer p2DBuffer;
hr = MFExtern.MFCreateDXGISurfaceBuffer(s_IID_ID3D11Texture2D, texNativeObject, 0, false, out p2DBuffer);
// Release the object-as-IUnknown we created above
COMBase.SafeRelease(texNativeObject);
// If media buffer creation failed, throw an exception
MFError.ThrowExceptionForHR(hr);
// Set the owning instance of this class as the allocator
// for IMFTrackedSample to notify when the sample is released
pTrackedSample.SetAllocator(this, null);
// Attach the created buffer to the sample
pTrackedSample.AddBuffer(p2DBuffer);
return pTrackedSample;
}
// This is public so any textures you allocate but don't make IMFSamples
// out of can be returned to the allocator manually.
public void ReturnFreeTexture(SharpDX.Direct3D11.Texture2D freeTexture)
{
m_freeStack.Push(freeTexture);
}
// IMFAsyncCallback.GetParameters
// This is allowed to return E_NOTIMPL as a way of specifying
// there are no special parameters.
public HResult GetParameters(out MFAsync pdwFlags, out MFAsyncCallbackQueue pdwQueue)
{
pdwFlags = MFAsync.None;
pdwQueue = MFAsyncCallbackQueue.Standard;
return HResult.E_NOTIMPL;
}
public HResult Invoke(IMFAsyncResult pResult)
{
object pUnkObject;
IMFSample pSample = null;
IMFMediaBuffer pBuffer = null;
IMFDXGIBuffer pDXGIBuffer = null;
// Get the IUnknown out of the IMFAsyncResult if there is one
HResult hr = pResult.GetObject(out pUnkObject);
if(Succeeded(hr))
{
pSample = pUnkObject as IMFSample;
}
if(pSample != null)
{
// Based on your implementation, there should only be one
// buffer attached to one sample, so we can always grab the
// first buffer. You could add some error checking here to make
// sure the sample has a buffer count that is 1.
hr = pSample.GetBufferByIndex(0, out pBuffer);
}
if(Succeeded(hr))
{
// Query the IMFMediaBuffer to see if it implements IMFDXGIBuffer
pDXGIBuffer = pBuffer as IMFDXGIBuffer;
}
if(pDXGIBuffer != null)
{
// Got an IMFDXGIBuffer, so we can extract the internal
// ID3D11Texture2D and make a new SharpDX.Texture2D wrapper.
hr = pDXGIBuffer.GetResource(s_IID_ID3D11Texture2D, out pUnkObject);
}
if(Succeeded(hr))
{
// If we got here, pUnkObject is the native D3D11 Texture2D as
// a System.Object, but it's unlikely you have an interface
// definition for ID3D11Texture2D handy, so we can't just cast
// the object to the proper interface.
// Happily, SharpDX supports wrapping System.Object within
// SharpDX.ComObject which makes things pretty easy.
SharpDX.ComObject comWrapper = new SharpDX.ComObject(pUnkObject);
// If this doesn't work, or you're using something like SlimDX
// which doesn't support object wrapping the same way, the below
// code is an alternative way.
/*
IntPtr pD3DTexture2D = Marshal.GetIUnknownForObject(pUnkObject);
// Create your wrapper object here, like this for SharpDX
SharpDX.ComObject comWrapper = new SharpDX.ComObject(pD3DTexture2D);
// or like this for SlimDX
SlimDX.Direct3D11.Texture2D.FromPointer(pD3DTexture2D);
Marshal.Release(pD3DTexture2D);
*/
// You might need to query comWrapper for a SharpDX.DXGI.Resource
// first, then query that for the SharpDX.Direct3D11.Texture2D.
SharpDX.Direct3D11.Texture2D texture = comWrapper.QueryInterface<SharpDX.Direct3D11.Texture2D>();
if(texture != null)
{
// Now you can add "texture" back to the allocator's free list
ReturnFreeTexture(texture);
}
}
}
}
Setting MF_SA_D3D_AWARE
关于 Sink Writer 输入媒体类型
我不认为这会造成不好的结果HRESULT
你得到了,但无论如何这都不是正确的做法。MF_SA_D3D_AWARE
(及其 DX11 对应项,MF_SA_D3D11_AWARE
)是由设置的属性IMFTransform
对象通知您该变换分别支持通过 DX9 或 DX11 的图形加速。无需在 Sink Writer 的输入媒体类型上设置此项。
No SafeRelease
on texNativeObject
我建议打电话COMBase.SafeRelease()
on texNativeObject
否则你可能会泄漏内存。否则,您将不必要地延长该 COM 对象的生命周期,直到 GC 为您清理引用计数为止
不必要的铸造
这是上面代码的一部分:
buffer = MFVideoEncoderST.ReinterpretCast<IMF2DBuffer,IMFMediaBuffer>(p2Dbuffer);
int length=0;
if (Succeeded(hr)) hr = (int)p2Dbuffer.GetContiguousLength(out length);
if (Succeeded(hr)) hr = (int)buffer.SetCurrentLength(length);
我不确定你的是什么ReinterpretCast
函数正在做,但是如果你do需要执行一个QueryInterface
在 C# 中进行样式转换,您可以使用as
运算符或常规演员。
// pMediaBuffer is of type IMFMediaBuffer and has been created elsewhere
IMF2DBuffer p2DBuffer = pMediaBuffer as IMF2DBuffer;
if(p2DBuffer != null)
{
// pMediaBuffer is an IMFMediaBuffer that also implements IMF2DBuffer
}
else
{
// pMediaBuffer does not implement IMF2DBuffer
}