Videos per SourceReader der MediaFoundation lesen

Mein letzter Beitrag auf dieser Seite hatte das Schreiben von Videos per Media Foundation zum Thema, in diesem hier geht es entsprechend um das Lesen. Auch hier habe ich mich an die Vorlagen von Microsoft gehalten, beispielsweise dieses kurze Tutorial [1]. Die Klasse SourceReader der Media Foundation ist hierbei Dreh- und Angelpunkt. Mit ihr kann man eine neue Video-Datei anlegen und entsprechend alle Video-Frames einzeln übergeben. Alles nichts weltbewegendes, letzten Endes hat es mir aber trotzdem ein paar Tage gekostet, die Sache ordentlich zum Laufen zu kriegen. Wer denkt auch daran, dass Alpha-Werte vom SourceReader ignoriert werden?

Doch bevor ich mich über die Probleme auslasse, noch ein paar allgemeinere Worte. Endresultat dieses Themas ist die Klasse MediaFoundationVideoReader, welche entsprechend über das Projekt Seeing# von jedem abgerufen werden kann [2]. Die Klasse ist so ausgelegt, dass sie im Konstruktor einen Zeiger auf eine Videodatei bekommt (hier repräsentiert durch ResourceLink). Die Videodatei wird direkt geöffnet und kann mit der Methode ReadFrame Bild für Bild ausgelesen werden. Wie üblich verwende ich die eigene Klasse MemoryMappedTexture32bpp als Speicherblock für die übertragenen Pixel-Daten.

Kommen wir zu den Problemen. Zunächst hatte ich mit der Tatsache zu kämpfen, dass ich anstelle eines Dateipfades direkt einen Stream übergeben möchte. Warum einen Stream? Damit der VideoReader unabhänigig von einer Datei im Dateisystem arbeiten kann. Warum ein Problem? Weil der SourceReader der Media Foundation u. A. auch den Dateinamen zur Erkennung des zu verwendenden Codecs betrachtet. Wie im nachfolgenden Codeausschnitt zu sehen, kann man aber trotzdem per Attribut einen „Dummy“-Dateinamen an den Stream der Media Foundation anhängen, somit kennt sich der SourceReader wieder aus.

// Create the source reader
using (MF.MediaAttributes mediaAttributes = new MF.MediaAttributes(1))
{
    // We need the 'EnableVideoProcessing' attribute because of the RGB32 format
    // see (lowest post): http://msdn.developer-works.com/article/11388495/How+to+use+SourceReader+(for+H.264+to+RGB+conversion)%3F
    mediaAttributes.Set(MF.SourceReaderAttributeKeys.EnableVideoProcessing, 1);
    mediaAttributes.Set(MF.SourceReaderAttributeKeys.DisableDxva, 1);

    // Wrap the .net stream to a MF Bytestream
    m_videoSourceStreamNet = m_videoSource.OpenInputStream();
    m_videoSourceStream = new MF.ByteStream(m_videoSourceStreamNet);
    using(MF.MediaAttributes byteStreamAttributes = m_videoSourceStream.QueryInterface<MF.MediaAttributes>())
    {
        byteStreamAttributes.Set(MF.ByteStreamAttributeKeys.OriginName, "Dummy." + videoSource.FileExtension);
    }

    // Create the sourcereader by custom native method (needed because of the ByteStream arg)
    IntPtr sourceReaderPointer = IntPtr.Zero;
    SharpDX.Result sdxResult = NativeMethods.MFCreateSourceReaderFromByteStream_Native(
        m_videoSourceStream.NativePointer,
        mediaAttributes.NativePointer,
        out sourceReaderPointer);
    sdxResult.CheckError();

    m_sourceReader = new MF.SourceReader(sourceReaderPointer);
}

Ein weiterer Punkt zum vorangegangenen Codeausschnitt: Ganz am Anfang setze ich die Attribute EnableVideoProcessing und DisableDxva. Diese dienen dazu, dass ich am Ende Bilder im RBG32 Format bekomme, denn nicht jeder Codec unterstützt das Lesen in dieses Format direkt. Die beiden Attribute kümmern sich entsprechend darum, dass der SourceReader im Hintergrund automatisch konvertiert.

Das zweite Problem hatte ich dann innerhalb meiner ReadFrame Methode, und zwar an der Stelle, an der der Inhalt des aktuellen Sample-Buffers der Media Foundation (= aktuell gelesenes Bild) in meinen eigenen Speicherblock kopiert wird. Zunächst habe ich hierzu die Methode MediaFactory.CopyImage von SharpDX verwendet [3]. Das Endresultat war, dass sich das ReadFrame an sich korrekt verhalten hat: Zeitstempel wurden korrekt gelesen, Anzahl Bytes im Speicherblock haben gepasst und End-of-File wurde nach der richtigen Anzahl Frames erreicht. Nur: Das von mir generierte Bitmap war immer Schwarz bzw. völlig transparent. Irgendwas musste also an den Bilddaten nicht passen, welche von dem SourceReader geliefert wurden. Letzten Endes war das Problem aber viel trivialer: Ich initialisiere meinen eigenen Speicherblock immer komplett mit 0 (für Rot-, Grün-, Blau- und Alpha-Kanal). Das CopyImage der Media Foundation ignoriert den Alpha-Kanal aber und somit bleibt dieser bei mir immer 0 und mein generiertes Bitmap bleibt durchsichtig – obwohl Rot-, Grün- und Blau-Werte passen. Auf sowas muss man kommen. Nachfolgend noch der ganze Codeausschnitt zum Lesen von Bildern aus dem SourceReader.

/// <summary>
/// Reads the next frame and puts it into the provided buffer.
/// </summary>
/// <param name="targetBuffer">The target buffer to write to.</param>
public bool ReadFrame(MemoryMappedTexture32bpp targetBuffer)
{
    this.EnsureNotNullOrDisposed("this");
    targetBuffer.EnsureNotNull("targetBuffer");
    if((targetBuffer.Width != m_frameSize.Width) ||
       (targetBuffer.Height != m_frameSize.Height))
    {
        throw new SeeingSharpGraphicsException("Size of the given buffer does not match the video size!");
    }

    MF.SourceReaderFlags readerFlags;
    int dummyStreamIndex;
    using (MF.Sample nextSample = m_sourceReader.ReadSample(
        MF.SourceReaderIndex.FirstVideoStream,
        MF.SourceReaderControlFlags.None,
        out dummyStreamIndex,
        out readerFlags,
        out m_currentPositionLong))
    {
        // Check for end-of-stream
        if (readerFlags == MF.SourceReaderFlags.Endofstream)
        {
            m_endReached = true;
            return false;
        }

        // No sample received
        if(nextSample == null)
        {
            return false;
        }

        // Reset end-reached flag (maybe the user called SetPosition again..)
        m_endReached = false;

        // Copy pixel data into target buffer
        if (nextSample.BufferCount > 0)
        {
            using (MF.MediaBuffer mediaBuffer = nextSample.ConvertToContiguousBuffer())
            {
                int cbMaxLength;
                int cbCurrentLenght;
                IntPtr mediaBufferPointer = mediaBuffer.Lock(out cbMaxLength, out cbCurrentLenght);
                try
                {
                    unsafe
                    {
                        int* mediaBufferPointerNative = (int*)mediaBufferPointer.ToPointer();
                        int* targetBufferPointerNative = (int*)targetBuffer.Pointer.ToPointer();
                        for (int loopY = 0; loopY < m_frameSize.Height; loopY++)
                        {
                            for (int loopX = 0; loopX < m_frameSize.Width; loopX++)
                            {
                                int actIndex = loopX + (loopY * m_frameSize.Width);
                                targetBufferPointerNative[actIndex] = mediaBufferPointerNative[actIndex];
                            }
                        }
                    }
                }
                finally
                {
                    mediaBuffer.Unlock();
                }

                // Apply 1 to all alpha values (these are not changed by the video frame!)
                targetBuffer.SetAllAlphaValuesToOne();

                return true;
            }
        }
    }

    return false;
}

Verweise

  1. https://msdn.microsoft.com/de-de/library/windows/desktop/dd389281(v=vs.85).aspx
  2. https://github.com/RolandKoenig/SeeingSharp/blob/master/SeeingSharp.Multimedia/DrawingVideo/_Readers/MediaFoundationVideoReader.cs
  3. http://sharpdx.org/documentation/api/m-sharpdx-mediafoundation-mediafactory-copyimage

1 Gedanke zu „Videos per SourceReader der MediaFoundation lesen“

  1. Hello, please help, in DuplicateDesctop function, may I duplicatedOutput.AcquireNextFrame(500, out FrameInfo, out screenResource);
    How get frame in NV12 format, and play his on WinForms any Panel ?

    Antworten

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.