Ошибка записи образца MF SinkWriter

Я пытаюсь закодировать ID3D11Texture2D в mp4 с помощью MediaFoundation. Ниже мой текущий код.

Инициализация модуля записи приемника

private int InitializeSinkWriter(String outputFile, int videoWidth, int videoHeight)
    {
        IMFMediaType mediaTypeIn = null;
        IMFMediaType mediaTypeOut = null;
        IMFAttributes attributes = null;

        int hr = 0;

        if (Succeeded(hr)) hr = (int)MFExtern.MFCreateAttributes(out attributes, 1);
        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);

        // Create the sink writer 
        if (Succeeded(hr)) hr = (int)MFExtern.MFCreateSinkWriterFromURL(outputFile, null, attributes, out sinkWriter);

        // Create the output type
        if (Succeeded(hr)) hr = (int)MFExtern.MFCreateMediaType(out mediaTypeOut);
        if (Succeeded(hr)) hr = (int)mediaTypeOut.SetGUID(MFAttributesClsid.MF_MT_MAJOR_TYPE, MFMediaType.Video);
        if (Succeeded(hr)) hr = (int)mediaTypeOut.SetGUID(MFAttributesClsid.MF_TRANSCODE_CONTAINERTYPE, MFTranscodeContainerType.MPEG4);
        if (Succeeded(hr)) hr = (int)mediaTypeOut.SetGUID(MFAttributesClsid.MF_MT_SUBTYPE, MFMediaType.H264);
        if (Succeeded(hr)) hr = (int)mediaTypeOut.SetUINT32(MFAttributesClsid.MF_MT_AVG_BITRATE, videoBitRate);
        if (Succeeded(hr)) hr = (int)mediaTypeOut.SetUINT32(MFAttributesClsid.MF_MT_INTERLACE_MODE, (int)MFVideoInterlaceMode.Progressive);            

        if (Succeeded(hr)) hr = (int)MFExtern.MFSetAttributeSize(mediaTypeOut, MFAttributesClsid.MF_MT_FRAME_SIZE, videoWidth, videoHeight);
        if (Succeeded(hr)) hr = (int)MFExtern.MFSetAttributeRatio(mediaTypeOut, MFAttributesClsid.MF_MT_FRAME_RATE, VIDEO_FPS, 1);
        if (Succeeded(hr)) hr = (int)MFExtern.MFSetAttributeRatio(mediaTypeOut, MFAttributesClsid.MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
        if (Succeeded(hr)) hr = (int)sinkWriter.AddStream(mediaTypeOut, out streamIndex);



        // Create the input type 
        if (Succeeded(hr)) hr = (int)MFExtern.MFCreateMediaType(out mediaTypeIn);
        if (Succeeded(hr)) hr = (int)mediaTypeIn.SetGUID(MFAttributesClsid.MF_MT_MAJOR_TYPE, MFMediaType.Video);
        if (Succeeded(hr)) hr = (int)mediaTypeIn.SetGUID(MFAttributesClsid.MF_MT_SUBTYPE, MFMediaType.ARGB32);
        if (Succeeded(hr)) hr = (int)mediaTypeIn.SetUINT32(MFAttributesClsid.MF_SA_D3D11_AWARE, 1);
        if (Succeeded(hr)) hr = (int)mediaTypeIn.SetUINT32(MFAttributesClsid.MF_MT_INTERLACE_MODE, (int)MFVideoInterlaceMode.Progressive);
        if (Succeeded(hr)) hr = (int)MFExtern.MFSetAttributeSize(mediaTypeIn, MFAttributesClsid.MF_MT_FRAME_SIZE, videoWidth, videoHeight);
        if (Succeeded(hr)) hr = (int)MFExtern.MFSetAttributeRatio(mediaTypeIn, MFAttributesClsid.MF_MT_FRAME_RATE, VIDEO_FPS, 1);
        if (Succeeded(hr)) hr = (int)MFExtern.MFSetAttributeRatio(mediaTypeIn, MFAttributesClsid.MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
        if (Succeeded(hr)) hr = (int)sinkWriter.SetInputMediaType(streamIndex, mediaTypeIn, null);


        // Start accepting data
        if (Succeeded(hr)) hr = (int)sinkWriter.BeginWriting();


        COMBase.SafeRelease(mediaTypeOut);
        COMBase.SafeRelease(mediaTypeIn);

        return hr;
    }

Рамка для письма

 int hr = 0;
        IMFSample sample = null;
        IMFMediaBuffer buffer = null;
        IMF2DBuffer p2Dbuffer = null;
        object texNativeObject = Marshal.GetObjectForIUnknown(surface.NativePointer);

        if (Succeeded(hr)) hr = (int)MFExtern.MFCreateDXGISurfaceBuffer(new Guid("6f15aaf2-d208-4e89-9ab4-489535d34f9c"), texNativeObject, 0, false, out p2Dbuffer);

        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);


        if (Succeeded(hr)) hr = (int)MFExtern.MFCreateVideoSampleFromSurface(null, out sample);

        if (Succeeded(hr)) hr = (int)sample.AddBuffer(buffer);
        if (Succeeded(hr)) hr = (int)sample.SetSampleTime(prevRecordingDuration);
        if (Succeeded(hr)) hr = (int)sample.SetSampleDuration((recordDuration - prevRecordingDuration));

        if (Succeeded(hr)) hr = (int)sinkWriter.WriteSample(streamIndex, sample);


        COMBase.SafeRelease(sample);
        COMBase.SafeRelease(buffer);

используя MFTRACE, я получаю следующую ошибку.

    02:48:04.99463 CMFSinkWriterDetours::WriteSample @024BEA18 Stream Index 0x0, Sample @17CEACE0, Time 571ms, Duration 16ms, Buffers 1, Size 4196352B,2088,2008 02:48:04.99465 CMFSinkWriterDetours::WriteSample @024BEA18 failed hr=0x887A0005 (null)2088,2008 
02:48:05.01090 CMFSinkWriterDetours::WriteSample @024BEA18 Stream Index 0x0, Sample @17CE9FC0, Time 587ms, Duration 17ms, Buffers 1, Size 4196352B,2088,2008 02:48:05.01091 CMFSinkWriterDetours::WriteSample @024BEA18 failed hr=0x887A0005 (null)2088,2008 
02:48:05.02712 CMFSinkWriterDetours::WriteSample @024BEA18 Stream Index 0x0, Sample @17CEACE0, Time 604ms, Duration 16ms, Buffers 1, Size 4196352B,2088,2008 02:48:05.02713 CMFSinkWriterDetours::WriteSample @024BEA18 failed hr=0x887A0005 (null)

Может ли кто-нибудь сказать мне, что не так с моим кодом? Я могу создать только 0 байт файла mp4.


person kripto    schedule 07.06.2017    source источник
comment
FYI 0x887A0005 означает, что экземпляр устройства GPU был приостановлен. Используйте GetDeviceRemovedReason, чтобы определить соответствующее действие. Ваш код, похоже, не запускает действительный сеанс аппаратного кодирования, но я бы сказал, однако, что реальная проблема заключается в текстуре вашего кадра - ее свойства и/или время жизни несовместимы с требованиями кодировщика.   -  person Roman R.    schedule 07.06.2017


Ответы (2)


Здесь у меня возникает несколько потенциальных проблем. Роман упомянул два больших, поэтому я остановлюсь на них подробнее. У меня есть еще пара критических замечаний/предложений для вас.

Не использую IMFDXGIDeviceManager

Чтобы использовать аппаратное ускорение в Media Foundation, вам необходимо создать объект диспетчера устройств DirectX, либо IDirect3DDeviceManager9 для DX9 или, в вашем случае, IMFDXGIDeviceManager для DXGI. Я настоятельно рекомендую прочитать всю документацию MSDN по этому интерфейсу. Причина, по которой это необходимо, заключается в том, что одно и то же устройство DX должно совместно использоваться всеми взаимодействующими аппаратными преобразованиями MF, поскольку всем им требуется доступ к общей памяти графического процессора, контролируемой устройством, и каждому из них требуется эксклюзивный контроль над устройством во время его работы. , поэтому нужна система блокировки. Объект диспетчера устройств обеспечивает эту систему блокировки, а также является стандартным способом предоставления устройства 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 экземпляров одной и той же текстуры

Это также является потенциальной причиной вашей проблемы - по крайней мере, это приведет к большому количеству непреднамеренного поведения, даже если оно не является причиной очевидной проблемы. Основываясь на комментарии Романа, многие кодировщики будут буферизовать несколько кадров как часть процесса кодирования. Вы не видите такого поведения при использовании Sink Writer, потому что он обрабатывает всю детальную работу за вас. Однако то, что вы пытаетесь выполнить (т. е. отправлять текстуры D3D11 в качестве входных кадров), находится на достаточно низком уровне, и вам приходится беспокоиться о внутренних деталях кодировщика MFT, используемого Sink Writer.

Большинство видеокодировщиков MFT используют внутренний буфер определенного размера для хранения последних N образцов, предоставленных через IMFTransform::ProcessInput. Это имеет побочный эффект, заключающийся в том, что несколько выборок должны быть предоставлены в качестве входных данных, прежде чем будет сгенерирован какой-либо вывод. Видеокодерам необходим доступ к нескольким образцам по порядку, потому что они используют последующие кадры, чтобы определить, как кодировать текущий кадр. Другими словами, если декодер работает с кадром 0, ему может потребоваться также просмотреть кадры 1, 2 и 3. С технической точки зрения это связано с такими вещами, как межкадровое предсказание и оценка движения. Как только кодировщик закончит обработку самой старой выборки, он создаст выходной буфер (еще один объект IMFSample, но на этот раз на стороне вывода через IMFTransform::ProcessOutput), затем отбрасывает входной образец, над которым он работал (вызывая IUnknown::Release), затем запрашивает дополнительные входные данные и в конечном итоге переходит к к следующему кадру. Подробнее об этом процессе можно прочитать в статье MSDN Обработка данных в кодировщике

Это означает, как намекнул Роман, что вы инкапсулируете ID3D11Texture2D внутри IMFMediaBuffer внутри IMFSample, а затем передаете это Sink Writer. Этот образец, вероятно, буферизуется кодировщиком как часть процесса кодирования. По мере работы энкодера содержимое этого Texture2D, вероятно, меняется, что может вызвать множество проблем. Даже если бы это не вызвало программных ошибок, это, безусловно, привело бы к очень странным закодированным видеовыходам. Представьте себе, что кодировщик пытается предсказать, как визуальное содержимое одного кадра изменится в следующем кадре, а затем фактическое визуальное содержимое обоих кадров обновляется из-под кодировщика!

Эта конкретная проблема возникает из-за того, что кодировщик имеет только ссылку указателя на ваш экземпляр IMFSample, который в конечном итоге является просто указателем на ваш объект ID3D11Texture2D, и этот объект является своего рода ссылкой указателя на изменяемую графическую память. В конечном счете, содержимое этой графической памяти изменяется из-за какой-то другой части вашей программы, но поскольку всегда обновляется одна и та же текстура графического процессора, каждый отдельный образец, который вы отправляете кодировщику, указывает на одну и ту же единственную текстуру. Это означает, что всякий раз, когда вы обновляете текстуру, изменяя память графического процессора, все активные объекты IMFSample будут отражать эти изменения, поскольку все они фактически указывают на одну и ту же текстуру графического процессора.

Чтобы исправить это, вам нужно выделить несколько объектов ID3D11Texture2D, чтобы вы могли соединить одну текстуру с одним IMFSample при предоставлении его Sink Writer. Это решит проблему, когда все сэмплы указывают на одну и ту же текстуру графического процессора, заставив каждый сэмпл указывать на уникальную текстуру. Однако вы не обязательно будете знать, сколько текстур вам нужно создать, поэтому самый безопасный способ справиться с этим — написать собственный распределитель текстур. Это все еще можно сделать в C#, для чего это стоит, MediaFoundation.NET имеет определенные интерфейсы, которые вам нужно будет использовать.

Распределитель должен поддерживать список «свободных» SharpDX.Texture2D объектов — тех, которые в данный момент не используются модулем записи/кодировщиком приемника. Ваша программа должна иметь возможность запрашивать новые объекты текстуры из распределителя, и в этом случае она либо вернет объект из списка свободных, либо создаст новую текстуру для удовлетворения запроса.

Следующая проблема заключается в том, чтобы узнать, когда объект IMFSample был отброшен кодировщиком, чтобы вы могли добавить прикрепленную текстуру обратно в список свободных. Как оказалось, функция MFCreateVideoSampleFromSurface, которую вы сейчас используете, выделяет образцы, реализующие IMFTrackedSample. Вам понадобится этот интерфейс, чтобы получать уведомления об освобождении образца, чтобы вы могли вернуть Texture2D объектов.

Хитрость заключается в том, что вы должны сообщить образцу, что вы являетесь распределителем. Во-первых, ваш класс распределителя должен реализовать 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);
           }
        }
    }
}


Настройка 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() по телефону texNativeObject, иначе может произойти утечка памяти. Это, или вы продлите время жизни этого COM-объекта без необходимости, пока сборщик мусора не очистит счетчик ссылок для вас


Ненужный кастинг

Это часть вашего кода сверху:

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, но если вам делает нужно выполнить приведение типа 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
}
person ozeanix    schedule 07.06.2017
comment
Спасибо за очень подробное объяснение. после изменения моего кода и добавления dxgidevice теперь я могу вывести результат, но есть небольшая проблема, и я знаю, что решение находится в вашем посте. Проблема в том, что я не знаю, как это реализовать. Я прочитал документацию в MSDN, но они не предоставляют коды, написанные на С#. К сожалению, я не эксперт в С++. Я не могу заставить работать IMFAsyncCallback. Вы также правы в том, что кодерам требуется больше входных данных перед обработкой вывода. Я проверил это, создав новый экземпляр texture2d для каждого образца. - person kripto; 08.06.2017
comment
Я добавлю небольшой пример, чтобы немного лучше объяснить модель распределителя. - person ozeanix; 08.06.2017
comment
Спасибо @ozeanix. Я использую SharpDX и MediaFoundation.NET. - person kripto; 08.06.2017
comment
Обновил свой пост с примером. Не каждая деталь заполнена, но этого должно быть достаточно для начала. - person ozeanix; 08.06.2017
comment
IMFTrackedSample не содержит определения «AddBuffer». я должен прикрепить буфер к IMFSample вместо IMFTrackedSample? и вернуть IMFSample вместо IMFTrackedSample? Это ошибка в CreateSampleAndAllocateTexture. - person kripto; 08.06.2017
comment
Я не уверен, почему не вызывает метод вызова. он продолжает выделять новую текстуру. Я создал новый распределитель текстур и вызвал метод CreateSampleAndAllocateTexture(), чтобы получить образец, а затем передать его стокрайтеру. - person kripto; 08.06.2017
comment
Моя ошибка в отслеживаемом образце - я думал, что он также реализует IMFSample. Наоборот, некоторые экземпляры IMFSample также будут реализовывать интерфейс IMFTrackedSample, поэтому вы правы, что вам нужно вызвать AddBuffer для IMFSample. Что касается того, почему у вас не вызывается метод TextureAllocator.Invoke, вы можете провести быстрый тест самостоятельно, выделив образец с помощью CreateSampleAndAllocateTexture(), а затем вызвав SafeRelease для образца. Это наверняка должно вызвать обратный вызов Invoke. - person ozeanix; 08.06.2017
comment
Нужно ли выпускать каждый образец после выделения новой текстуры? Поскольку я понимаю, что синкрайтеру требуется несколько сэмплов, прежде чем он сгенерирует вывод. Если я не выпускаю образец, он продолжает выделять новую текстуру и никогда не запускает метод вызова. Моя логика внутри цикла AcquireNextFrame и Query Texture2D и CreateSampleAndAllocateTexture и передача их в SinkWriter. моя логика неверна? - person kripto; 09.06.2017
comment
Вам следует SafeRelease IMFSample после того, как вы отправили его SinkWriter через WriteSample. После того, как вы это сделаете, когда SinkWriter выпустит образец, он должен вызвать IMFAsyncCallback в реализации TextureAllocator, которая восстановит D3D-текстуру и добавит ее обратно в пул. - person ozeanix; 09.06.2017
comment
Я посмотрю более подробно завтра, но похоже, что кодировщик Intel H264 по какой-то причине не используется, и он переключается на кодировщик Microsoft H264, который не поддерживает DXGI / DX11, поэтому образцы, которые вы отправляете, могут не обрабатываться должным образом. Я не знаю, может ли видеопроцессор MFT брать образцы DXGI и преобразовывать их в буфер системной памяти для использования преобразования без поддержки DX11. - person ozeanix; 09.06.2017
comment
Мой первоначальный подход заключается в создании буфера памяти, сохранении текстуры в этом буфере и передаче буфера в образец. проблема в том, что процессор становится настолько мощным, что хорошо, что видеозапись плавная и может записывать 60 кадров в секунду. Но я хочу добиться аппаратного кодирования. - person kripto; 09.06.2017
comment
Я только что заметил в журнале MFTrace, что вы не устанавливаете MF_SINK_WRITER_D3D_MANAGER при создании SinkWriter, вы отправляете MF_SOURCE_READER_D3D_MANAGER, с которым он не знает, что делать. Попробуйте переключить его и посмотрите, что произойдет. Я не думаю, что этого достаточно, чтобы предотвратить загрузку кодировщика Intel H264, но это определенно не помогает. - person ozeanix; 09.06.2017
comment
Кроме того, в MFTrace есть ошибка, о которой я забыл, заключающаяся в том, что кодировщик Intel H264 никогда не загружается, пока вы используете MFTrace, но он отлично работает, когда вы запускаете свою программу сам по себе. Я почти уверен, что это тоже часть проблемы - вам может понадобиться отладить это без использования MFTrace. - person ozeanix; 09.06.2017
comment
@kripto, тебе пока что повезло? Вы пытались установить MF_SINK_WRITER_D3D_MANAGER и это что-то исправило? - person ozeanix; 13.06.2017
comment
да. MF_SINK_WRITER_D3D_MANAGER исходит от SharpDX, который является SharpDX.DXGI.Device. - person kripto; 13.06.2017
comment
Правильно, но одна из проблем заключалась в том, что вы ранее устанавливали MF_SOURCE_READER_D3D_MANAGER на IMFSinkWriter, что неверно. Мне было любопытно, привело ли исправление к каким-либо улучшениям. - person ozeanix; 13.06.2017
comment
После вызова int hr = (int)MFExtern.MFStartup(0x00020070, MFStartup.Full); я вызываю этот метод. pastebin.com/AWq0jzTn и это весь код, как я инициализирую синкрайтер. Я не знаю, почему он продолжает устанавливать MF_SOURCE_READER_D3D_MANAGER вместо MF_SINK_WRITER_D3D_MANAGER - person kripto; 13.06.2017
comment
Я рад, что вы опубликовали все это, я вижу еще одну проблему. Ваш диспетчер устройств — это не диспетчер устройств, а просто устройство DXGI. Это неправильно. Вам необходимо создать диспетчер устройств с MFExtern.MFCreateDXGIDeviceManager (ссылка на MSDN). Пожалуйста, прочтите эту ссылку - в ней обсуждается, как создать диспетчер устройств DXGI и как подключить устройство Direct3D11 к диспетчеру устройств, вызвав ResetDevice. Диспетчер устройств DXGI — это то, что позволяет вам совместно использовать устройство D3D11 через преобразования. - person ozeanix; 13.06.2017
comment
Как только вы создадите диспетчер устройств DXGI, установите его на устройстве записи приемника с тем же кодом, который вы используете, но следующим образом: IMFDXGIDeviceManager pDeviceManager; int resetToken; MFExtern.MFCreateDXGIDeviceManager(out resetToken, out pDeviceManager); После этого вы сбрасываете диспетчер устройств с помощью созданного вами устройства D3D11. См. здесь ResetDevice - person ozeanix; 13.06.2017
comment
Пара других вещей. Возможно, вы захотите установить attributes.SetUINT32(MFAttributesClsid.MF_SINK_WRITER_DISABLE_THROTTLING, 1);, чтобы модуль записи приемника не ограничивал входящие сэмплы. Кроме того, MF_SOURCE_READER_D3D_MANAGER — это тот же GUID, что и MF_SINK_WRITER_D3D_MANAGER, так что здесь все в порядке — MFTrace просто предпочитает исходное имя читателя для этого GUID. Моя ошибка в этом. Наконец, один прием, который вы можете попробовать, так как в MFTrace есть ошибка с кодировщиком Intel HW. Используйте профилировщик Visual Studio с включенной встроенной отладкой. Посмотрите, появится ли что-то вроде mfx_mft_h264ve. - person ozeanix; 13.06.2017
comment
Я получаю сообщение об ошибке Invalid_Arg при перезагрузке устройства. это мой новый код pastebin.com/0XLsF8A8 - person kripto; 13.06.2017
comment
Вам нужно передать ResetDevice объект, реализующий ID3D11Device, а не устройство DXGI. Таким образом, аргумент функции в вашем pastebin должен быть SharpDX.Direct3D11.Device вместо этого. При создании этого устройства обязательно ознакомьтесь с рекомендациями на странице ResetDevice документации, потому что есть несколько важных моментов. У вас должен быть установлен флаг создания D3D11_CREATE_DEVICE_VIDEO_SUPPORT, а также необходимо настроить многопоточность на устройстве при его создании. - person ozeanix; 13.06.2017
comment
Спасибо, что указали на это, но, к сожалению, я получаю ту же ошибку. Замените аргумент DXGI на SharpDX.Direct3D11.Device, а также установите флаг создания SharpDX.Direct3D11.DeviceCreationFlags.VideoSupport. Я также сделал для MultiThreadProtection значение true. все еще возвращает E_INVALIDARG. Я не уверен, что мне не хватает. - person kripto; 13.06.2017
comment
Я буду думать об этом. Я посмотрю, смогу ли я приготовить какой-нибудь тестовый код на своей стороне, чтобы увидеть, получу ли я те же сбои от IMFDXGIDeviceManager.ResetDevice. Если вы можете, опубликуйте весь код настройки устройства D3D11 в pastebin, чтобы я мог видеть, что там происходит. Очевидно, внутри ResetDevice происходит что-то очень странное, и единственными параметрами являются устройство D3D11 и токен сброса. Я почти уверен, что это не проблема с токеном сброса при просмотре вашего кода, поэтому это должно быть что-то не так с самим устройством. - person ozeanix; 13.06.2017
comment
@kripto Ух ты! Это было немного больше работы, чем я думал. Я решил эту проблему на 100%, я генерирую выходные данные .mp4 с разрешением 1920x1080@60 кадров в секунду с помощью аппаратного кодировщика Intel. Было довольно много проблем, которые нужно было решить, чтобы довести его до этого момента - я забыл, насколько чувствителен кодировщик Intel к настройке IMFSinkWriter. Я также повторно включил некоторые вещи, которые вы закомментировали, например, повторное использование существующих экземпляров Texture2D. Если вы не возражаете, я опубликую еще один ответ на этот вопрос, чтобы у меня было больше места, чтобы объяснить, что именно я изменил и почему. Я также выложу фиксированный код для вас. - person ozeanix; 14.06.2017
comment
Вау, спасибо большое. вы действительно так хорошо знаете mediafoundation и SharpDX. ты замечательный. Как, кстати, загрузка процессора? он потребляет только ниже 10%? - person kripto; 14.06.2017
comment
Спасибо за комплименты. Мне также пришлось изучить несколько новых вещей, я на самом деле лучше знаком с DX9 по сравнению с DX11, поэтому мне пришлось выяснить, как сбросить Texture2D в промежуточную текстуру, а затем сбросить ее в файл .png. Я оставил код отладки с некоторыми флагами, чтобы включать и выключать его, чтобы вы тоже могли видеть, как это работает. Я буду работать над написанием другого ответа, как я уже сказал, я постараюсь пройтись по всему коду по пунктам, чтобы вы могли видеть, что было нужно и что я изменил. Вероятно, это будет для вас завтра вместе с фиксированным кодом в .zip. - person ozeanix; 14.06.2017
comment
Загрузка ЦП варьируется от 2,5% до 7% на процессоре ноутбука Intel i7-4510U с тактовой частотой 2,84 ГГц. Я бы сказал, что это очень хорошо! - person ozeanix; 14.06.2017
comment
идеально! Я очень рад видеть это. - person kripto; 14.06.2017
comment
Привет @ozeanix. Есть ли у вас какие-либо сведения о том, как я могу записывать звуки как на микрофон, так и на вывод звука с помощью медиафонда? Я планирую использовать Naudio, потому что он доступен в Shardx. Попробую реализовать в этом проекте. Благодарность - person kripto; 17.06.2017
comment
привет @ozeanix, @kripto. Я очень ценю ваш вклад в эту тему. это помогает мне легко понять весь процесс. Я шаг за шагом основывался на этом. Но до сих пор я все еще застрял, как сделать функцию InternalAllocateNewTexture. Попробуйте получить его из кадров, снятых с экрана. Все в порядке, за исключением получения ошибки Exception от HRESULT: 0x80041000 от sinWriter.WriteSample. Трудно найти, какой из них неправильный, поэтому поделитесь со мной любыми рабочими кодами, если это возможно. - person vominhtien961476; 30.12.2019

Первая проблема: IMFDXGIDeviceManager::ResetDevice всегда терпит неудачу.

После работы с @kripto в комментариях к моему предыдущему ответу мы диагностировали множество других проблем. Самой большой проблемой была настройка IMFDXGIDeviceManager, чтобы позволить аппаратному кодировщику H.264 MFT принимать образцы Direct3D11 Texture2D, содержащиеся внутри IMFDXGIBuffer. В коде была очень трудно заметить ошибку:

// pDevice is a SharpDX.Direct3D11.Texture2D instance
// pDevice.NativePointer is an IntPtr that refers to the native IDirect3D11Device
// being wrapped by SharpDX.
IMFDXGIDeviceManager pDeviceManager;
object d3dDevice = Marshal.GetIUnknownForObject(pDevice.NativePointer);
HResult hr = MFExtern.MFCreateDXGIDeviceManager(out resetToken, out pDeviceManager);
if(Succeeded(hr))
{
    // The signature of this is:
    // HResult ResetDevice(object d3d11device, int resetToken);
    hr = pDeviceManager.ResetDevice(d3dDevice, resetToken);
}

Вот что происходит в приведенном выше коде. Диспетчер устройств создан, но для того, чтобы кодировщик MFT мог получить доступ к Texture2D семплам, ему нужна копия того же Direct3D-устройства, которое создало текстуры. Поэтому вам необходимо вызвать IMFDXGIDeviceManager::ResetDevice в диспетчере устройств, чтобы предоставить ему устройство Direct3D. См. [1] для некоторых важных сносок на ResetDevice. SharpDX предоставляет доступ только к IntPtr, указывающему на собственный IDirect3D11Device, но интерфейс MediaFoundation.NET требует вместо этого передачи object.

Ошибка еще не видна? Приведенный выше код проверяет тип и компилируется нормально, но содержит критическую ошибку. Ошибка заключалась в использовании Marshal.GetIUnknownForObject вместо Marshal.GetObjectForIUnknown. Забавно, что поскольку object может прекрасно упаковывать IntPtr, вы можете использовать прямо противоположную функцию маршалинга, и она все равно прекрасно компилируется. Проблема в том, что мы пытаемся преобразовать IntPtr в .NET RCW внутри object, чего и ожидает ResetDevice из MediaFoundation.NET. Эта ошибка заставила ResetDevice вернуть E_INVALIDARG вместо правильной работы.


Вторая проблема: странный вывод кодировщика

Вторая проблема заключалась в том, что Intel Quick Sync Video H.264 Encoder MFT особо не радовал, и хотя он создавался корректно, в начале результирующего файла была секунда-две черного вывода, а также блокировка и ошибки движения в течение первых нескольких секунд, иногда половина видео была серой и не отображала фактическое дублированное изображение рабочего стола.

Я хотел убедиться, что фактические объекты Texture2D правильно отправляются кодировщику, поэтому я написал простой класс для дампа Texture2D Direct3D 11 в файл .png. Я включил это здесь для всех, кому это нужно — для работы требуются SharpDX и MediaFoundation.NET, хотя вы можете удалить зависимость MF, используя CopyMemory в цикле для учета различных шагов. Обратите внимание, что это настроено только для работы с текстурами в DXGI.Format.B8G8R8A8_UNorm. Возможно, он будет работать с текстурами в других форматах, но результат будет выглядеть очень странно.

using System;
using System.Drawing;

namespace ScreenCapture
{
    class Texture2DDownload : IDisposable
    {
        private SharpDX.Direct3D11.Device m_pDevice;
        private SharpDX.Direct3D11.Texture2D m_pDebugTexture;

        public Texture2DDownload(SharpDX.Direct3D11.Device pDevice)
        {
            m_pDevice = pDevice;
        }

        /// <summary>
        /// Compare all the relevant properties of the texture descriptions for both input textures.
        /// </summary>
        /// <param name="texSource">The source texture</param>
        /// <param name="texDest">The destination texture that will have the source data copied into it</param>
        /// <returns>true if the source texture can be copied to the destination, false if their descriptions are incompatible</returns>
        public static bool TextureCanBeCopied(SharpDX.Direct3D11.Texture2D texSource, SharpDX.Direct3D11.Texture2D texDest)
        {
            if(texSource.Description.ArraySize != texDest.Description.ArraySize)
                return false;

            if(texSource.Description.Format != texDest.Description.Format)
                return false;

            if(texSource.Description.Height != texDest.Description.Height)
                return false;

            if(texSource.Description.MipLevels != texDest.Description.MipLevels)
                return false;

            if(texSource.Description.SampleDescription.Count != texDest.Description.SampleDescription.Count)
                return false;

            if(texSource.Description.SampleDescription.Quality != texDest.Description.SampleDescription.Quality)
                return false;

            if(texSource.Description.Width != texDest.Description.Width)
                return false;

            return true;
        }

        /// <summary>
        /// Saves the contents of a <see cref="SharpDX.Direct3D11.Texture2D"/> to a file with name contained in <paramref name="filename"/> using the specified <see cref="System.Drawing.Imaging.ImageFormat"/>.
        /// </summary>
        /// <param name="texture">The <see cref="SharpDX.Direct3D11.Texture2D"/> containing the data to save.</param>
        /// <param name="filename">The filename on disk where the output image should be saved.</param>
        /// <param name="imageFormat">The format to use when saving the output file.</param>
        public void SaveTextureToFile(SharpDX.Direct3D11.Texture2D texture, string filename, System.Drawing.Imaging.ImageFormat imageFormat)
        {
            // If the existing debug texture doesn't exist, or the incoming texture is different than the existing debug texture...
            if(m_pDebugTexture == null || !TextureCanBeCopied(m_pDebugTexture, texture))
            {
                // Dispose of any existing texture
                if(m_pDebugTexture != null)
                {
                    m_pDebugTexture.Dispose();
                }

                // Copy the original texture's description...
                SharpDX.Direct3D11.Texture2DDescription newDescription = texture.Description;

                // Then modify the parameters to create a CPU-readable staging texture
                newDescription.BindFlags = SharpDX.Direct3D11.BindFlags.None;
                newDescription.CpuAccessFlags = SharpDX.Direct3D11.CpuAccessFlags.Read;
                newDescription.OptionFlags = SharpDX.Direct3D11.ResourceOptionFlags.None;
                newDescription.Usage = SharpDX.Direct3D11.ResourceUsage.Staging;

                // Re-generate the debug texture by copying the new texture's description
                m_pDebugTexture = new SharpDX.Direct3D11.Texture2D(m_pDevice, newDescription);
            }

            // Copy the texture to our debug texture
            m_pDevice.ImmediateContext.CopyResource(texture, m_pDebugTexture);

            // Map the debug texture's resource 0 for read mode
            SharpDX.DataStream data;
            SharpDX.DataBox dbox = m_pDevice.ImmediateContext.MapSubresource(m_pDebugTexture, 0, 0, SharpDX.Direct3D11.MapMode.Read, SharpDX.Direct3D11.MapFlags.None, out data);

            // Create a bitmap that's the same size as the debug texture
            Bitmap b = new Bitmap(m_pDebugTexture.Description.Width, m_pDebugTexture.Description.Height, System.Drawing.Imaging.PixelFormat.Format32bppRgb);

            // Lock the bitmap data to get access to the native bitmap pointer
            System.Drawing.Imaging.BitmapData bd = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppRgb);

            // Use the native pointers to do a native-to-native memory copy from the mapped subresource to the bitmap data
            // WARNING: This might totally blow up if you're using a different color format than B8G8R8A8_UNorm, I don't know how planar formats are structured as D3D textures!
            //
            // You can use Win32 CopyMemory to do the below copy if need be, but you have to do it in a loop to respect the Stride and RowPitch parameters in case the texture width
            // isn't on an aligned byte boundary.
            MediaFoundation.MFExtern.MFCopyImage(bd.Scan0, bd.Stride, dbox.DataPointer, dbox.RowPitch, bd.Width * 4, bd.Height);

            /// Unlock the bitmap
            b.UnlockBits(bd);

            // Unmap the subresource mapping, ignore the SharpDX.DataStream because we don't need it.
            m_pDevice.ImmediateContext.UnmapSubresource(m_pDebugTexture, 0);
            data = null;

            // Save the bitmap to the desired filename
            b.Save(filename, imageFormat);
            b.Dispose();
            b = null;
        }

        #region IDisposable Support
        private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
        {
            if(!disposedValue)
            {
                if(disposing)
                {
                }

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

                disposedValue = true;
            }
        }

        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
        ~Texture2DDownload() {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(false);
        }

        // This code added to correctly implement the disposable pattern.
        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }
}

Как только я убедился, что хорошие изображения находятся на пути к кодировщику, я обнаружил, что код не вызывает IMFSinkWriter::SendStreamTick после вызова IMFSinkWriter::BeginWriting, но перед отправкой первого IMFSample. Исходный образец также имел ненулевую временную дельту, которая вызывала начальный черный вывод. Чтобы исправить это, я добавил следующий код:

// Existing code to set the sample time and duration
// recordDuration is the current frame time in 100-nanosecond units
// prevRecordingDuration is the frame time of the last frame in
// 100-nanosecond units
sample.SetSampleTime(recordDuration);
sample.SetSampleDuration(recordDuration - prevRecordingDuration);

// The fix is here:
if(frames == 0)
{
    sinkWriter.SendStreamTick(streamIndex, recordDuration);
    sample.SetUINT32(MFAttributesClsid.MFSampleExtension_Discontinuity, 1);
}

sinkWriter.WriteSample(streamIndex, sample);
frames++;

Отправляя метку потока в модуль записи приемника, он устанавливает, что любое значение в recordDuration теперь считается точкой времени = 0 для выходного видеопотока. Другими словами, когда вы вызываете SetStreamTick и передаете временную метку кадра, из всех последующих временных меток вычитается исходная временная метка. Таким образом вы сразу же отобразите первый образец кадра в выходном файле.

Кроме того, всякий раз, когда вызывается SendStreamTick, образец, передаваемый средству записи приемника сразу после него, должен иметь MFSampleExtension_Discontinuity имеет значение 1 в своих атрибутах. Это означает, что в отправляемых выборках был пробел, и кадр, передаваемый кодировщику, является первым кадром после этого пробела. Это более или менее говорит кодировщику сделать ключевой кадр из сэмпла, что предотвращает эффекты движения и блокировки, которые я видел раньше. <ч/>

Полученные результаты

После внесения этих исправлений я протестировал приложение и добился полноэкранного захвата с разрешением 1920x1080 и частотой 60 кадров в секунду. Битрейт был установлен на 4096 кбит. Использование ЦП на ЦП ноутбука Intel i7-4510U составляло от 2,5% до 7% для большинства рабочих нагрузок — при экстремальном движении оно могло достигать примерно 10%. Использование графического процессора через SysInternals Process Explorer составляло от 1% до 2%.


[1] Я полагаю, что часть этого является пережитком Direct3D 9, когда поддержка многопоточности не была хорошо встроена в API DirectX, и устройство должно было быть монопольно заблокировано всякий раз, когда какой-либо компонент использовал его ( то есть декодер, рендерер, кодировщик). Используя D3D 9, вы вызываете ResetDevice, но затем больше никогда не сможете использовать свой собственный указатель на устройство. Вместо этого вы должны вызывать LockDevice и UnlockDevice даже в своем собственном коде, чтобы получить указатель устройства, потому что MFT может использовать устройство в тот же момент. В Direct3D 11, похоже, нет проблем с одновременным использованием одного и того же устройства в MFT и в управляющем приложении, хотя, если случаются какие-либо случайные сбои, я бы посоветовал много прочитать о том, как работают IMFDXGIDeviceManager::LockDevice и UnlockDevice, и внедрить их, чтобы убедиться. устройство всегда контролируется исключительно.

person ozeanix    schedule 14.06.2017
comment
где мы можем связаться с вами? - person kripto; 15.06.2017
comment
@kripto моя электронная почта - лучший способ, [email protected], спасибо! - person ozeanix; 15.06.2017
comment
Отправил вам письмо. Спасибо. Дайте мне знать, если вы находитесь внутри. - person kripto; 15.06.2017
comment
Привет @ozeanix, извините, что снова беспокою вас, но у меня проблема с d3ddevice. При запуске приложения в полноэкранном режиме я получаю сообщение "Утеряно устройство". Поэтому я повторно инициализирую дублированный рабочий стол, а также устройство d3d, но моя запись не продолжается. Если вы все еще помните, мы использовали MFCreateDXGIDeviceManager и передали d3ddevice. Можно ли продолжить запись при переинициализации d3ddevice? - person kripto; 27.10.2017