Videos per SinkWriter der MediaFoundation schreiben

Aktuell arbeite ich in Vorbereitung für ein kommendes (Hobby-)Projekt wieder verstärkt mit der Media Foundation von MS. Erster Schritt war nun, per SinkWriter Videos direkt aus der 3D-Ansicht von Seeing# heraus schreiben zu können. Der zweite Schritt, Videos auch wieder auszulesen, ist noch in Arbeit [1]. Der SinkWriter ist eine Klasse der Media Foundation, welche dazu verwendet werden kann, von der Anwendung generierte Bilder direkt in eine Videodatei zu schreiben. Soll ein Video also etwa 30 Frames pro Sekunde haben, so kann die Anwendung eben diese 30 Frames pro Sekunde erzeugen und an den SinkWriter übergeben. Mein aktueller Stand ist damit der, dass ich direkt aus dem 3D-Rendering heraus nebenbei ein Video schreiben lassen kann.

Im Wesentlichen habe ich mich bei der Umsetzung an diesem Tutorial von Microsoft orientiert [2]. Herausgekommen sind die Klassen SeeingSharpVideoWriter und die entsprechenden Ableitungen davon, wie z. B. der Mp4VideoWriter [3]. Probleme hatte ich bei der Umsetzung eher wenige, das Größte ist aktuell noch die Integration für WinRT, da scheinbar das Kopieren von Speicherblöcken dort etwas anders programmiert werden muss. Weiterhin war es nicht ganz einfach, eine funktionierende Kombination von Parametern für den SinkWriter zu finden. Stellt man etwa Bitrate, Auflösung und Framerate so ein, dass sie für das verwendete Format nicht zusammenpassen, so bekommt man entweder beim Anlegen des SinkWriters einen Fehler oder dann erst im Anschluss beim Schreiben der Frames.

Die Konfiguration des SinkWriters bez. des Input-Formats (=Format der Daten aus der eigenen Anwendung. Hier: RGB32) befindet sich in der Klasse MediaFoundationVideoWriter:

/// <summary>
/// Starts rendering to the target.
/// </summary>
/// <param name="videoPixelSize">The pixel size of the video.</param>
protected override void StartRenderingInternal(Size2 videoPixelSize)
{
    m_outStreamNet = base.TargetFile.OpenOutputStream();
    m_outStream = new MF.ByteStream(m_outStreamNet);

    // Pass dummy filename as described here:
    // https://social.msdn.microsoft.com/forums/windowsapps/en-us/49bffa74-4e84-4fd6-9d67-42e8385611b8/video-sinkwriter-in-metro-app
    m_sinkWriter = MF.MediaFactory.CreateSinkWriterFromURL(
        this.DummyFileName, m_outStream.NativePointer, null);
    m_videoPixelSize = videoPixelSize;

    CreateMediaTarget(m_sinkWriter, m_videoPixelSize, out m_streamIndex);

    // Configure input
    using (MF.MediaType mediaTypeIn = new MF.MediaType())
    {
        mediaTypeIn.Set<Guid>(MF.MediaTypeAttributeKeys.MajorType, MF.MediaTypeGuids.Video);
        mediaTypeIn.Set<Guid>(MF.MediaTypeAttributeKeys.Subtype, VIDEO_INPUT_FORMAT);
        mediaTypeIn.Set<int>(MF.MediaTypeAttributeKeys.InterlaceMode, (int)MF.VideoInterlaceMode.Progressive);
        mediaTypeIn.Set<long>(MF.MediaTypeAttributeKeys.FrameSize, MFHelper.GetMFEncodedIntsByValues(videoPixelSize.Width, videoPixelSize.Height));
        mediaTypeIn.Set<long>(MF.MediaTypeAttributeKeys.FrameRate, MFHelper.GetMFEncodedIntsByValues(m_framerate, 1));
        m_sinkWriter.SetInputMediaType(m_streamIndex, mediaTypeIn, null);
    }

    // Start writing the video file
    m_sinkWriter.BeginWriting();

    // Set initial frame index
    m_frameIndex = -1;
}

Die Konfiguration des Output-Formats (=Format des zu schreibenden Videos, hier MP4) befindet sich dann in der abgeleiteten Klasse Mp4VideoWriter. Diese verschiedenen Klassen sind deswegen entstanden, da ich pro Dateiformat evtl. auch weitere Zusatzeinstellungen etwas anders zu treffen habe. Hier der Code für das MP4 Format:

/// <summary>
/// Creates a media target.
/// </summary>
/// <param name="sinkWriter">The previously created SinkWriter.</param>
/// <param name="videoPixelSize">The pixel size of the video.</param>
/// <param name="streamIndex">The stream index for the new target.</param>
protected override void CreateMediaTarget(MF.SinkWriter sinkWriter, Size2 videoPixelSize, out int streamIndex)
{
    using (MF.MediaType mediaTypeOut = new MF.MediaType())
    {
        mediaTypeOut.Set<Guid>(MF.MediaTypeAttributeKeys.MajorType, MF.MediaTypeGuids.Video);
        mediaTypeOut.Set<Guid>(MF.MediaTypeAttributeKeys.Subtype, VIDEO_ENCODING_FORMAT);
        mediaTypeOut.Set<int>(MF.MediaTypeAttributeKeys.AvgBitrate, base.Bitrate * 1000);
        mediaTypeOut.Set<int>(MF.MediaTypeAttributeKeys.InterlaceMode, (int)MF.VideoInterlaceMode.Progressive);
        mediaTypeOut.Set<long>(MF.MediaTypeAttributeKeys.FrameSize, MFHelper.GetMFEncodedIntsByValues(videoPixelSize.Width, videoPixelSize.Height));
        mediaTypeOut.Set<long>(MF.MediaTypeAttributeKeys.FrameRate, MFHelper.GetMFEncodedIntsByValues(base.Framerate, 1));
        sinkWriter.AddStream(mediaTypeOut, out streamIndex);
    }
}

Wenn man genau hinschaut, sieht man, dass die Konfiguration des Output-Formats (zweiter Codeblock) direkt vor der Konfiguration des Input-Formats (erster Codeblock) aufgerufen wird. Wenn alles konfiguriert ist, ist nur noch der Aufruf von m_sinkWriter.BeginWriting() nötig.

Das Schreiben der Frames selbst ist unabhängig vom Output-Format und wie folgt kodiert.

/// <summary>
/// Draws the given frame to the video.
/// </summary>
/// <param name="device">The device on which the given framebuffer is created.</param>
/// <param name="uploadedTexture">The texture which should be added to the video.</param>
protected override void DrawFrameInternal(EngineDevice device, MemoryMappedTexture32bpp uploadedTexture)
{
    // Cancel here if the given texture has an invalid size
    if (m_videoPixelSize != new Size2(uploadedTexture.Width, uploadedTexture.Height)) { return; }

    m_frameIndex++;
    MF.MediaBuffer mediaBuffer = MF.MediaFactory.CreateMemoryBuffer((int)uploadedTexture.SizeInBytes);
    try
    {
        // Write all contents to the MediaBuffer for media foundation
        int cbMaxLength = 0;
        int cbCurrentLength = 0;
        IntPtr mediaBufferPointer = mediaBuffer.Lock(out cbMaxLength, out cbCurrentLength);
        try
        {

            int stride = m_videoPixelSize.Width * 4;
            if (this.FlipY)
            {
                for(int loop=0 ; loop<m_videoPixelSize.Height; loop++)
                {
                    MF.MediaFactory.CopyImage(
                        mediaBufferPointer + (m_videoPixelSize.Height - (1 + loop)) * stride,
                        stride,
                        uploadedTexture.Pointer + loop * stride,
                        stride,
                        stride,
                        1);
                }
            }
            else
            {
                MF.MediaFactory.CopyImage(
                    mediaBufferPointer,
                    stride,
                    uploadedTexture.Pointer,
                    stride,
                    stride,
                    m_videoPixelSize.Height);
            }
        }
        finally
        {
            mediaBuffer.Unlock();
        }
        mediaBuffer.CurrentLength = (int)uploadedTexture.SizeInBytes;

        // Create the sample (includes image and timing information)
        MF.Sample sample = MF.MediaFactory.CreateSample();
        try
        {
            sample.AddBuffer(mediaBuffer);

            long frameDuration = 10 * 1000 * 1000 / m_framerate;
            sample.SampleTime = frameDuration * m_frameIndex;
            sample.SampleDuration = frameDuration;

            m_sinkWriter.WriteSample(m_streamIndex, sample);
        }
        finally
        {
            GraphicsHelper.SafeDispose(ref sample);
        }
    }
    finally
    {
        GraphicsHelper.SafeDispose(ref mediaBuffer);
    }
}

Besonders anzumerken an diesem Code-Block ist die Abfrage if(this.FlipY) in der Mitte. Diese ist schlicht dadurch entstanden, weil WMV-Videos korrekt und MP4-Dateien spiegelverkehrt geschrieben wurden. Aus diesem Grund habe ich kurzerhand diese Abfrage eingebaut… ich möchte aber nicht ausschließen, dass es da auch irgendwo eine elegantere Lösung dafür gibt. Aber so reicht es mir erst einmal.

Die Integration in das 3D-Rendering habe ich dann so gelöst, dass man diesen VideoWriter direkt der RenderLoop Klasse übergeben kann. Folgende Testmethode zeigt das recht anschaulich.

[Fact]
[Trait("Category", TEST_CATEGORY)]
public async Task RenderSimple_Mp4Video()
{
    await UnitTestHelper.InitializeWithGrahicsAsync();

    // Configure video rendering
    string videoFilePath = Path.Combine(Environment.CurrentDirectory, "Video.wmv");
    try
    {
        Mp4VideoWriter videoWriter = new Mp4VideoWriter(videoFilePath);
        videoWriter.Bitrate = 1500;

        // Perform video rendering
        await RenderSimple_Generic(videoWriter, 100);

        // Check results
        Assert.True(File.Exists(videoFilePath));
    }
    finally
    {
        if (File.Exists(videoFilePath))
        {
            File.Delete(videoFilePath);
        }
    }
}

Verweise

  1. http://www.mycsharp.de/wbb2/thread.php?threadid=114414
  2. https://msdn.microsoft.com/de-de/library/windows/desktop/ff819477(v=vs.85).aspx 
  3. https://github.com/RolandKoenig/SeeingSharp/tree/master/SeeingSharp.Multimedia/DrawingVideo

Schreibe einen Kommentar

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