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

将你的动画带到 H264/HEVC 视频

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (45投票s)

2019年7月2日

MIT

9分钟阅读

viewsIcon

70038

downloadIcon

2892

使用 C++ 和 C# 通过硬件加速将您的动画转换为 H264/HEVC 视频

目录

引言

我最近完成了我的(闭源)照片幻灯片应用程序 Mandy Frenzy,该应用程序基于本文介绍的视频编码器。下面是由该应用程序编码的视频。您可以从 此处 下载我的幻灯片应用程序,或访问 其主页

去年,我为 OpenGL 推出了一款基于 Windows 的单头文件视频编码器,可在 Windows 7 及以上版本上运行。请参见上面的视频演示!我已经将其与 OpenGL 线程解耦,使其更容易编码 2D 帧。您只需逐帧填充帧缓冲区即可创建动画。在本文中,我使用了 GDI+,因为它对我来说最熟悉,但您也可以使用您喜欢的图形库;视频编码器与 GDI+ 无关。HEVC 编解码器过去曾随 Windows 10 一起捆绑,但现在 Microsoft 已将其移除,并在 Microsoft Store 中出售。那个 HEVC 编解码器存在质量问题,需要提供更高的比特率才能保持与 H264 编码视频相同的质量。开始写入视频文件之前,请确保视频文件未被视频播放器打开或锁定。新的 H264Writer 构造函数如下:

H264Writer(string mp3_file, string dest_file, VideoCodec codec, 
    int width, int height, int fps, int duration /*in milliseconds*/, 
    FrameRenderer* frameRenderer, uint bitrate);
H264Writer(const wchar_t* mp3_file, const wchar_t* dest_file, VideoCodec codec, 
    int width, int height, int fps, int duration /*in milliseconds*/, 
    FrameRenderer* frameRenderer, UINT32 bitrate = 4000000);

mp3_file 参数是 MP3 文件路径(如果不需要音频,可以为空),dest_file 参数是生成的视频文件。codec 参数可以是 H264HEVCwidthheight 参数指视频的宽度和高度。fps 参数是视频的每秒帧数,我通常指定为 30 或 60。duration 参数指视频时长(毫秒),可以设置为 -1 表示视频时长与 MP3 相同。bitrate 参数指每秒字节的视频比特率。请记住为高分辨率视频和 HEVC 设置更高的比特率。纯虚函数 FrameRendererRender 函数签名如下。widthheight 是视频尺寸。fps 是每秒帧数,而 frame_cnt 是帧计数,它会在每一帧自动递增。pixels 参数是用于填充您的位图数据的单维数组。返回值应为 false 表示出现灾难性错误,此时应停止编码。FrameRenderer 是一个类,其 Render 方法在每一帧被调用,它应该由用户实现。

interface FrameRenderer
{
    public bool Render(int width, int height, int fps, int frame_cnt, uint[] pixels);
};
class FrameRenderer
{
public:
    virtual bool Render(int width, int height, int fps, 
                        int frame_cnt, UINT32* pixels) = 0;
};

红色视频

对于我们的第一个示例,我们保持简单。我们只渲染一个红色视频。

Red Video

这是 main 函数,其中包含 H264Writer.h,实例化 H264Writer 并调用 Process() 来编码视频。Process() 调用给定的 Render()RenderRedImage

using H264WriterDLL;

static void Main(string[] args)
{
    string musicFile = "";
    string videoFile = "C:\\temp\\RedVideoCSharp.mp4";

    RenderRedImage frameRenderer = new RenderRedImage();

    H264Writer writer = new H264Writer(musicFile, videoFile, VideoCodec.H264, 
                            640, 480, 30, 5000, frameRenderer, 40000000);
    if (writer.IsValid())
    {
        if (writer.Process())
        {
            Console.WriteLine("Video written successfully!\n");
            return;
        }
    }
    Console.WriteLine("Video write failed!\n");
}
#include "../Common/H264Writer.h"

int main()
{
    std::wstring musicFile(L"");
    std::wstring videoFile(L"C:\\temp\\RedVideo.mp4");

    RenderRedImage frameRenderer;

    H264Writer writer(musicFile.c_str(), videoFile.c_str(), VideoCodec::H264, 
                      640, 480, 30, 5000, &frameRenderer);
    if (writer.IsValid())
    {
        if (writer.Process())
        {
            printf("Video written successfully!\n");
            return 0;
        }
    }
    printf("Video write failed!\n");
}

下面是 C++ RenderRedImage 类。它仅在 frame_cnt 为零时渲染,即仅在第一帧渲染,因为 pixels 保持不变,无需在每一帧中再次填充。

// render a red image once!
public class RenderRedImage : FrameRenderer
{
    public bool Render(int width, int height, int fps, int frame_cnt, UInt32[] pixels)
    {
        if (frame_cnt == 0)
        {
            for (int col = 0; col < width; ++col)
            {
                for (int row = 0; row < height; ++row)
                {
                    int index = row * width + col;
                    pixels[index] = 0xffff0000;
                }
            }
        }
        return true;
    }
}
// render a red image once!
class RenderRedImage : public FrameRenderer
{
public:
    bool Render(int width, int height, int fps, int frame_cnt, UINT32* pixels) override
    {
        if (frame_cnt == 0)
        {
            for (int col = 0; col < width; ++col)
            {
                for (int row = 0; row < height; ++row)
                {
                    int index = row * width + col;
                    pixels[index] = 0xffff0000;
                }
            }
        }
        return true;
    }
};

像素格式为 Alpha、Red、Green、Blue (ARGB) 格式。例如,如果您想要一个蓝色视频,只需更改为 pixels[index] = 0xff0000ff;

单张 JPEG 视频

对于第二个示例,我们使用 GDI+ 加载一个 JPEG 图像并渲染一次,即在 frame_cnt 为零时。

Video with 1 image

现在我们使用 GDI+,因此我们必须包含 Gdiplus.h 头文件及其 Gdiplus.libframeRenderer 类型现在设置为 RenderJPG 类。

using H264WriterDLL;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

static void Main(string[] args)
{
    string musicFile = "";
    string videoFile = "C:\\temp\\JpgVideoCSharp.mp4";

    RenderJPG frameRenderer = new RenderJPG();

    H264Writer writer = new H264Writer(musicFile, videoFile, VideoCodec.H264, 
                            640, 480, 30, 10000, frameRenderer, 40000000);
    if (writer.IsValid())
    {
        if (writer.Process())
        {
            Console.WriteLine("Video written successfully!\n");
            return;
        }
    }
    Console.WriteLine("Video write failed!\n");
}
#include "../Common/H264Writer.h"
#include <Gdiplus.h>
#pragma comment(lib, "gdiplus.lib")

int main()
{
    std::wstring musicFile(L"");
    std::wstring videoFile(L"C:\\temp\\JpgVideo.mp4");

    RenderJPG frameRenderer;
    H264Writer writer(musicFile.c_str(), videoFile.c_str(), VideoCodec::H264, 
                      640, 480, 30, 10000, &frameRenderer);
    if (writer.IsValid())
    {
        if (writer.Process())
        {
            printf("Video written successfully!\n");
            return 0;
        }
    }
    printf("Video write failed!\n");
}

对于熟悉 GDI+ 的开发者来说,RenderJPG 非常简单。它使用 GdiplusStartup()GdiplusShutdown 分别初始化和销毁 GDI+。它使用 Bitmap 类加载“yes.jpg”。bmp 是一个与视频尺寸相同的 Bitmap。我们使用 FillRectangle() 用黑色填充 bmp。然后我们计算 JPEG 文件和视频帧的纵横比。如果 w_ratio_jpg 大于 w_ratio_bmp,则意味着 image 比视频宽,因此您将在视频的顶部和底部看到两条水平黑色条,否则您将在视频的两侧看到两条垂直黑色条。换句话说,我们尝试尽可能地渲染图像以覆盖视频,同时保持其原始纵横比。为了获取 bmp 的像素指针,我们必须在使用后调用 LockBits(),并在使用后调用 UnlockBits()。您会注意到在双重 for 循环中,图像是垂直颠倒渲染的,这样它在视频输出中会正确显示。在 C# 版本的 RenderJPG 类中。与 C++ 版本相比,您无需在构造函数和析构函数中初始化和反初始化 GDI+,因为当引用 System.Drawing 库时,它已为开发者处理好了。

// render a jpeg once!
public class RenderJPG : FrameRenderer
{
    public bool Render(int width, int height, int fps, int frame_cnt, UInt32[] pixels)
    {
        if (frame_cnt == 0)
        {
            Bitmap bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
            Bitmap jpg = new Bitmap("image\\yes.jpg", true);
            Graphics g = Graphics.FromImage(bmp);
            float w_ratio_bmp = bmp.Width / (float)bmp.Height;
            float w_ratio_jpg = jpg.Width / (float)jpg.Height;

            SolidBrush brush = new SolidBrush(Color.Black);
            g.FillRectangle(brush, 0, 0, bmp.Width, bmp.Height);

            if (w_ratio_jpg >= w_ratio_bmp)
            {
                int width2 = bmp.Width;
                int height2 = (int)((bmp.Width / (float)jpg.Width) * jpg.Height);
                g.DrawImage(jpg, 0, (bmp.Height - height2) / 2, width2, height2);
            }
            else
            {
                int width2 = (int)((bmp.Height / (float)jpg.Height) * jpg.Width);
                int height2 = bmp.Height;
                g.DrawImage(jpg, (bmp.Width - width2) / 2, 0, width2, height2);
            }

            BitmapData bitmapData = new BitmapData();
            Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);

            bmp.LockBits(
                rect,
                ImageLockMode.ReadOnly,
                PixelFormat.Format32bppArgb,
                bitmapData);

            unsafe
            {
                uint* pixelsSrc = (uint*)bitmapData.Scan0;

                if (pixelsSrc==null)
                    return false;

                int stride = bitmapData.Stride >> 2;

                for (int col = 0; col < width; ++col)
                {
                    for (int row = 0; row < height; ++row)
                    {
                        int indexSrc = (height - 1 - row) * stride + col;
                        int index = row * width + col;
                        pixels[index] = pixelsSrc[indexSrc];
                    }
                }
            }
            bmp.UnlockBits(bitmapData);

            bmp.Dispose();
            jpg.Dispose();
            brush.Dispose();
            g.Dispose();
        }
        return true;
    }
}
// render a jpg once!
class RenderJPG : public FrameRenderer
{
public:
    RenderJPG()
    {
        // Initialize GDI+ so that we can load the JPG
        Gdiplus::GdiplusStartup(&m_gdiplusToken, &m_gdiplusStartupInput, NULL);
    }
    ~RenderJPG()
    {
        Gdiplus::GdiplusShutdown(m_gdiplusToken);
    }

    // render a jpg once!
    bool Render(int width, int height, int fps, int frame_cnt, UINT32* pixels) override
    {
        using namespace Gdiplus;

        if (frame_cnt == 0)
        {
            Bitmap bmp(width, height, PixelFormat32bppARGB);
            Bitmap jpg(L"image\\yes.jpg", TRUE);
            Graphics g(&bmp);
            float w_ratio_bmp = bmp.GetWidth() / (float)bmp.GetHeight();
            float w_ratio_jpg = jpg.GetWidth() / (float)jpg.GetHeight();

            SolidBrush brush(Color::Black);
            g.FillRectangle(&brush, 0, 0, bmp.GetWidth(), bmp.GetHeight());

            if (w_ratio_jpg >= w_ratio_bmp)
            {
                int width2 = bmp.GetWidth();
                int height2 = (int)((bmp.GetWidth() / 
                                    (float)jpg.GetWidth()) * jpg.GetHeight());
                g.DrawImage(&jpg, 0, (bmp.GetHeight() - height2) / 2, width2, height2);
            }
            else
            {
                int width2 = (int)((bmp.GetHeight() / 
                                   (float)jpg.GetHeight()) * jpg.GetWidth());
                int height2 = bmp.GetHeight();
                g.DrawImage(&jpg, (bmp.GetWidth() - width2) / 2, 0, width2, height2);
            }

            BitmapData bitmapData;
            Rect rect(0, 0, bmp.GetWidth(), bmp.GetHeight());

            bmp.LockBits(
                &rect,
                ImageLockModeRead,
                PixelFormat32bppARGB,
                &bitmapData);

            UINT* pixelsSrc = (UINT*)bitmapData.Scan0;

            if (!pixelsSrc)
                return false;

            int stride = bitmapData.Stride >> 2;

            for (int col = 0; col < width; ++col)
            {
                for (int row = 0; row < height; ++row)
                {
                    int indexSrc = (height - 1 - row) * stride + col;
                    int index = row * width + col;
                    pixels[index] = pixelsSrc[indexSrc];
                }
            }

            bmp.UnlockBits(&bitmapData);

        }
        return true;
    }

private:
    Gdiplus::GdiplusStartupInput m_gdiplusStartupInput;
    ULONG_PTR m_gdiplusToken;
};

两张 JPEG 视频

对于第三个示例,我们显示第一张图像,并逐渐将其与第二张图像进行 Alpha 混合,直到其出现。您可以通过观看视频来看到效果。

由于 main 函数与前面的完全相同,只是 frameRenderer 被设置为 Render2JPG,因此我们跳过显示它。

Render2JPGRenderJPG 几乎相同,只是它使用 Bitmap 类加载 2 张 JPEG 图片。当持续时间小于或等于 1000 毫秒时,存储在 alpha 变量中的透明度为零(完全透明),当持续时间大于或等于 2000 毫秒时,为 255(完全不透明)。在 1000 到 2000 毫秒的持续时间之间,会计算 alpha。关于 frame_duration = 1000 / fps 的一个小提示:由于它是整数,因此不精确。例如,当 fps 为 30 时:1000/30 得到 33 毫秒,但 30 * 33 只会得到 990 毫秒,而不是原始的 1000 毫秒。使用 fpsframe_cnt 来计算当前帧在视频中显示的时间至关重要。请勿使用计时器来计算经过的时间,因为编码每一帧所需的时间可能因渲染逻辑的复杂性而异。

// render 2 jpegs!
public class Render2JPG : FrameRenderer
{
   public Render2JPG(string img1, string img2)
    {
        g = null;
        g2 = null;
        bmp = null;
        bmp2 = null;
        jpg1 = new Bitmap(img1, true);
        jpg2 = new Bitmap(img2, true);
    }
    ~Render2JPG()
    {
        jpg1.Dispose();
        jpg2.Dispose();
        bmp.Dispose();
        bmp2.Dispose();
        g.Dispose();
        g2.Dispose();
    }

    public bool Render(int width, int height, int fps, int frame_cnt, UInt32[] pixels)
    {
        if (bmp == null)
        {
            bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
            bmp2 = new Bitmap(width, height, PixelFormat.Format32bppArgb);
            g = Graphics.FromImage(bmp);
            g2 = Graphics.FromImage(bmp2);
        }
        byte alpha = 0;
        float frame_duration = 1000.0f / fps;
        float time = frame_cnt * frame_duration;
        if (time <= 1000.0)
            alpha = 0;
        else if (time >= 2000.0)
            alpha = 255;
        else
            alpha = (byte)(int)(((time) - 1000.0) / 1000.0 * 255);

        float w_ratio_bmp = bmp.Width / (float)bmp.Height;
        float w_ratio_jpg = jpg1.Width / (float)jpg1.Height;

        SolidBrush brush = new SolidBrush(Color.Black);
        g.FillRectangle(brush, 0, 0, bmp.Width, bmp.Height);

        if (w_ratio_jpg >= w_ratio_bmp)
        {
            int width2 = bmp.Width;
            int height2 = (int)((bmp.Width / (float)jpg1.Width) * jpg1.Height);
            g.DrawImage(jpg1, 0, (bmp.Height - height2) / 2, width2, height2);
            g2.DrawImage(jpg2, 0, (bmp2.Height - height2) / 2, width2, height2);
        }
        else
        {
            int width2 = (int)((bmp.Height / (float)jpg1.Height) * jpg1.Width);
            int height2 = bmp.Height;
            g.DrawImage(jpg1, (bmp.Width - width2) / 2, 0, width2, height2);
            g2.DrawImage(jpg2, (bmp2.Width - width2) / 2, 0, width2, height2);
        }

        BitmapData bitmapData = new BitmapData();
        BitmapData bitmapData2 = new BitmapData();
        Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);

        bmp.LockBits(
            rect,
            ImageLockMode.ReadOnly,
            PixelFormat.Format32bppArgb,
            bitmapData);

        bmp2.LockBits(
            rect,
            ImageLockMode.ReadOnly,
            PixelFormat.Format32bppArgb,
            bitmapData2);

        unsafe
        {
            uint* pixelsSrc = (uint*)bitmapData.Scan0;
            uint* pixelsSrc2 = (uint*)bitmapData2.Scan0;

            if (pixelsSrc == null || pixelsSrc2 == null)
                return false;

            int stride = bitmapData.Stride >> 2;

            for (int col = 0; col < width; ++col)
            {
                for (int row = 0; row < height; ++row)
                {
                    int indexSrc = (height - 1 - row) * stride + col;
                    int index = row * width + col;
                    pixels[index] = Alphablend(pixelsSrc2[indexSrc], 
                                               pixelsSrc[indexSrc], alpha, 0xff);
                }
            }
        }
        bmp.UnlockBits(bitmapData);
        bmp2.UnlockBits(bitmapData2);

        brush.Dispose();

        return true;
    }

    private uint Alphablend(uint dest, uint source, byte nAlpha, byte nAlphaFinal)
    {
        byte nInvAlpha = (byte) ~nAlpha;

        byte nSrcRed = (byte)((source & 0xff0000) >> 16);
        byte nSrcGreen = (byte)((source & 0xff00) >> 8);
        byte nSrcBlue = (byte)(source & 0xff);

        byte nDestRed = (byte)((dest & 0xff0000) >> 16);
        byte nDestGreen = (byte)((dest & 0xff00) >> 8);
        byte nDestBlue = (byte)(dest & 0xff);

        byte nRed = (byte)((nSrcRed * nAlpha + nDestRed * nInvAlpha) >> 8);
        byte nGreen = (byte)((nSrcGreen * nAlpha + nDestGreen * nInvAlpha) >> 8);
        byte nBlue = (byte)((nSrcBlue * nAlpha + nDestBlue * nInvAlpha) >> 8);

        return (uint)(nAlphaFinal << 24 | nRed << 16 | nGreen << 8 | nBlue);
    }

    private Bitmap jpg1;
    private Bitmap jpg2;
    private Graphics g;
    private Graphics g2;
    private Bitmap bmp;
    private Bitmap bmp2;
}
// render 2 jpg
class Render2JPG : public FrameRenderer
{
public:
    Render2JPG(const std::wstring& img1, const std::wstring& img2)
    : jpg1(nullptr), jpg2(nullptr), g(nullptr), g2(nullptr), bmp(nullptr), bmp2(nullptr)
    {
        // Initialize GDI+ so that we can load the JPG
        Gdiplus::GdiplusStartup(&m_gdiplusToken, &m_gdiplusStartupInput, NULL);
        jpg1 = new Gdiplus::Bitmap(img1.c_str(), TRUE);
        jpg2 = new Gdiplus::Bitmap(img2.c_str(), TRUE);
    }
    ~Render2JPG()
    {
        delete jpg1;
        delete jpg2;
        delete bmp;
        delete bmp2;
        delete g;
        delete g2;
        Gdiplus::GdiplusShutdown(m_gdiplusToken);
    }

    // render 2 jpg
    // This function takes a long time.
    bool Render(int width, int height, int fps, int frame_cnt, UINT32* pixels) override
    {
        using namespace Gdiplus;

        if (bmp == nullptr)
        {
            bmp = new Bitmap(width, height, PixelFormat32bppARGB);
            bmp2 = new Bitmap(width, height, PixelFormat32bppARGB);
            g = new Graphics(bmp);
            g2 = new Graphics(bmp2);
        }
        BYTE alpha = 0;
        float frame_duration = 1000.0 / fps;
        float time = frame_cnt * frame_duration;
        if (time <= 1000.0)
            alpha = 0;
        else if (time >= 2000.0)
            alpha = 255;
        else
            alpha = (BYTE)(int)(((time)-1000.0) / 1000.0 * 255);

        float w_ratio_bmp = bmp->GetWidth() / (float)bmp->GetHeight();
        float w_ratio_jpg = jpg1->GetWidth() / (float)jpg1->GetHeight();

        SolidBrush brush(Color::Black);
        g->FillRectangle(&brush, 0, 0, bmp->GetWidth(), bmp->GetHeight());

        if (w_ratio_jpg >= w_ratio_bmp)
        {
            int width2 = bmp->GetWidth();
            int height2 = (int)((bmp->GetWidth() / 
                                (float)jpg1->GetWidth()) * jpg1->GetHeight());
            g->DrawImage(jpg1, 0, (bmp->GetHeight() - height2) / 2, width2, height2);
            g2->DrawImage(jpg2, 0, (bmp2->GetHeight() - height2) / 2, width2, height2);
        }
        else
        {
            int width2 = (int)((bmp->GetHeight() / 
                               (float)jpg1->GetHeight()) * jpg1->GetWidth());
            int height2 = bmp->GetHeight();
            g->DrawImage(jpg1, (bmp->GetWidth() - width2) / 2, 0, width2, height2);
            g2->DrawImage(jpg2, (bmp2->GetWidth() - width2) / 2, 0, width2, height2);
        }

        BitmapData bitmapData;
        BitmapData bitmapData2;
        Rect rect(0, 0, bmp->GetWidth(), bmp->GetHeight());

        bmp->LockBits(
            &rect,
            ImageLockModeRead,
            PixelFormat32bppARGB,
            &bitmapData);

        bmp2->LockBits(
            &rect,
            ImageLockModeRead,
            PixelFormat32bppARGB,
            &bitmapData2);

        UINT* pixelsSrc = (UINT*)bitmapData.Scan0;
        UINT* pixelsSrc2 = (UINT*)bitmapData2.Scan0;

        if (!pixelsSrc || !pixelsSrc2)
            return false;

        int stride = bitmapData.Stride >> 2;

        for (int col = 0; col < width; ++col)
        {
            for (int row = 0; row < height; ++row)
            {
                int indexSrc = (height - 1 - row) * stride + col;
                int index = row * width + col;
                pixels[index] = Alphablend(pixelsSrc2[indexSrc], 
                                           pixelsSrc[indexSrc], alpha, 0xff);
            }
        }

        bmp->UnlockBits(&bitmapData);
        bmp2->UnlockBits(&bitmapData2);

        return true;
    }

private:
    inline UINT Alphablend(UINT dest, UINT source, BYTE nAlpha, BYTE nAlphaFinal)
    {
        BYTE nInvAlpha = ~nAlpha;

        BYTE nSrcRed = (source & 0xff0000) >> 16;
        BYTE nSrcGreen = (source & 0xff00) >> 8;
        BYTE nSrcBlue = (source & 0xff);

        BYTE nDestRed = (dest & 0xff0000) >> 16;
        BYTE nDestGreen = (dest & 0xff00) >> 8;
        BYTE nDestBlue = (dest & 0xff);

        BYTE nRed = (nSrcRed * nAlpha + nDestRed * nInvAlpha) >> 8;
        BYTE nGreen = (nSrcGreen * nAlpha + nDestGreen * nInvAlpha) >> 8;
        BYTE nBlue = (nSrcBlue * nAlpha + nDestBlue * nInvAlpha) >> 8;

        return nAlphaFinal << 24 | nRed << 16 | nGreen << 8 | nBlue;
    }
    private:
    Gdiplus::GdiplusStartupInput m_gdiplusStartupInput;
    ULONG_PTR m_gdiplusToken;
    Gdiplus::Bitmap* jpg1;
    Gdiplus::Bitmap* jpg2;
    Gdiplus::Graphics* g;
    Gdiplus::Graphics* g2;
    Gdiplus::Bitmap* bmp;
    Gdiplus::Bitmap* bmp2;
};

pixels[index]Alphablend() 函数确定。

文本动画视频

在最后一个示例中,我们展示了两张预渲染的文本图像从视频中间出现。请参见下面的视频示例。

有一个细细的白色矩形在逐渐扩大。renderbmp 变量是其中显示 bmp 的上半部分和 bmp2 的下半部分。bmp 是用 jpg1 向上逐渐移动渲染的,而 bmp2 是用 jpg2 向下逐渐移动渲染的。jpg1jpg2 是误称,因为加载的图像实际上是 PNG。Bitmap 类可以加载 JPEG 和 PNG。JPEG 最适合存储照片,而 PNG 最适合存储插图。

// text animation!
public class RenderText : FrameRenderer
{
    public RenderText(string img1, string img2)
    {
        g = null;
        g2 = null;
        bmp = null;
        bmp2 = null;
        jpg1 = new Bitmap(img1, true);
        jpg2 = new Bitmap(img2, true);
    }
    ~RenderText()
    {
        jpg1.Dispose();
        jpg2.Dispose();
        bmp.Dispose();
        bmp2.Dispose();
        g.Dispose();
        g2.Dispose();
    }

    public bool Render(int width, int height, int fps, int frame_cnt, UInt32[] pixels)
    {
        if (bmp == null)
        {
            bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
            bmp2 = new Bitmap(width, height, PixelFormat.Format32bppArgb);
            g = Graphics.FromImage(bmp);
            g2 = Graphics.FromImage(bmp2);
        }
        Bitmap renderbmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
        Graphics render_g = Graphics.FromImage(renderbmp);

        float rectProgress = 0.0f;
        float textProgress = 0.0f;
        float frame_duration = 1000.0f / fps;
        float time = frame_cnt * frame_duration;

        SolidBrush brush = new SolidBrush(Color.Black);
        render_g.FillRectangle(brush, 0, 0, width, height);
        g.FillRectangle(brush, 0, 0, width, height);

        int rectHeight = 4;

        int rectWidth = (int)(width * 0.8f);
        if (time >= 1000.0f)
            rectProgress = 1.0f;
        else
            rectProgress = time / 1000.0f;


        if (time >= 2000.0f)
            textProgress = 1.0f;
        else if (time <= 1000.0f)
            textProgress = 0.0f;
        else
            textProgress = (time - 1000.0f) / 1000.0f;

        g.DrawImage(jpg1, (width - jpg1.Width) / 2, 
                          (height / 2) - (int)(jpg1.Height * textProgress), 
                          jpg1.Width, jpg1.Height);
        g.FillRectangle(brush, 0, height / 2 - 4, width, height / 2 + 4);
        render_g.DrawImage(bmp, 0, 0, width, height);

        g2.DrawImage(jpg2, (width - jpg2.Width) / 2, 
		                   (int)((height / 2 - jpg2.Height) + 
                           (int)(jpg2.Height * textProgress)), 
                           jpg2.Width, jpg2.Height);
        g2.FillRectangle(brush, 0, 0, width, height / 2 + 4);
        render_g.DrawImage(bmp2, 0, height / 2 + 4, 
                           new Rectangle(0, height / 2 + 4, width, height / 2 - 4), 
                           GraphicsUnit.Pixel);

        SolidBrush whitebrush = new SolidBrush(Color.White);
        int start_x = (width - (int)(rectWidth * rectProgress)) / 2;
        int pwidth = (int)(rectWidth * rectProgress);
        render_g.FillRectangle(whitebrush, start_x, 
                               (height - rectHeight) / 2, pwidth, 
                               rectHeight);

        BitmapData bitmapData = new BitmapData();
        Rectangle rect = new Rectangle(0, 0, width, height);

        renderbmp.LockBits(
            rect,
            ImageLockMode.ReadOnly,
            PixelFormat.Format32bppArgb,
            bitmapData);

        unsafe
        {
            uint* pixelsSrc = (uint*)bitmapData.Scan0;

            if (pixelsSrc == null)
                return false;

            int stride = bitmapData.Stride >> 2;

            for (int col = 0; col < width; ++col)
            {
                for (int row = 0; row < height; ++row)
                {
                    int indexSrc = (height - 1 - row) * stride + col;
                    int index = row * width + col;
                    pixels[index] = pixelsSrc[indexSrc];
                }
            }
        }
        renderbmp.UnlockBits(bitmapData);

        renderbmp.Dispose();
        brush.Dispose();
        whitebrush.Dispose();
        render_g.Dispose();

        return true;
    }

    private Bitmap jpg1;
    private Bitmap jpg2;
    private Graphics g;
    private Graphics g2;
    private Bitmap bmp;
    private Bitmap bmp2;
}
class RenderText : public FrameRenderer
{
public:
    RenderText(const std::wstring& img1, const std::wstring& img2) 
        : jpg1(nullptr), jpg2(nullptr), g(nullptr), 
          g2(nullptr), bmp(nullptr), bmp2(nullptr)
    {
        // Initialize GDI+ so that we can load the JPG
        Gdiplus::GdiplusStartup(&m_gdiplusToken, &m_gdiplusStartupInput, NULL);
        jpg1 = new Gdiplus::Bitmap(img1.c_str(), TRUE);
        jpg2 = new Gdiplus::Bitmap(img2.c_str(), TRUE);
    }
    ~RenderText()
    {
        delete jpg1;
        delete jpg2;
        delete bmp;
        delete bmp2;
        delete g;
        delete g2;
        Gdiplus::GdiplusShutdown(m_gdiplusToken);
    }


    // render text
    bool Render(int width, int height, int fps, int frame_cnt, UINT32* pixels) override
    {
        using namespace Gdiplus;

        if (bmp == nullptr)
        {
            bmp = new Bitmap(width, height, PixelFormat32bppARGB);
            bmp2 = new Bitmap(width, height, PixelFormat32bppARGB);
            g = new Graphics(bmp);
            g2 = new Graphics(bmp2);
        }

        Bitmap renderbmp(width, height, PixelFormat32bppARGB);
        Graphics render_g(&renderbmp);

        float rectProgress = 0.0f;
        float textProgress = 0.0f;
        float frame_duration = 1000.0f / fps;
        float time = frame_cnt * frame_duration;

        SolidBrush brush(Color::Black);
        render_g.FillRectangle(&brush, 0, 0, width, height);
        g->FillRectangle(&brush, 0, 0, width, height);

        int rectHeight = 4;

        int rectWidth = (int)(width * 0.8f);
        if (time >= 1000.0f)
            rectProgress = 1.0f;
        else
            rectProgress = time / 1000.0f;

        if (time >= 2000.0f)
            textProgress = 1.0f;
        else if (time <= 1000.0f)
            textProgress = 0.0f;
        else
            textProgress = (time - 1000.0f) / 1000.0f;

        g->DrawImage(jpg1, (width - jpg1->GetWidth()) / 2, 
                        (height / 2) - (int)(jpg1->GetHeight() * textProgress), 
                        jpg1->GetWidth(), jpg1->GetHeight());
        g->FillRectangle(&brush, 0, height / 2 - 4, width, height / 2 + 4);
        render_g.DrawImage(bmp, 0, 0, width, height);

        g2->DrawImage(jpg2, (width - jpg2->GetWidth()) / 2, 
                               (int)((height / 2 - jpg2->GetHeight()) + 
                               (int)(jpg2->GetHeight() * textProgress)), 
                               jpg2->GetWidth(), jpg2->GetHeight());
        g2->FillRectangle(&brush, 0, 0, width, height / 2 + 4);
        render_g.DrawImage(bmp2, 0, height / 2 + 4, 0, height / 2 + 4, width, 
                           height / 2 - 4, Gdiplus::UnitPixel);

        SolidBrush whitebrush(Color::White);
        int start_x = (width - (int)(rectWidth * rectProgress)) / 2;
        int pwidth = (int)(rectWidth * rectProgress);
        render_g.FillRectangle(&whitebrush, start_x, (height - rectHeight) / 2, 
                               pwidth, rectHeight);

        BitmapData bitmapData;
        Rect rect(0, 0, width, height);

        renderbmp.LockBits(
            &rect,
            ImageLockModeRead,
            PixelFormat32bppARGB,
            &bitmapData);

        UINT* pixelsSrc = (UINT*)bitmapData.Scan0;

        if (!pixelsSrc)
            return false;

        int stride = bitmapData.Stride >> 2;

        for (int col = 0; col < width; ++col)
        {
            for (int row = 0; row < height; ++row)
            {
                int indexSrc = (height - 1 - row) * stride + col;
                int index = row * width + col;
                pixels[index] = pixelsSrc[indexSrc];
            }
        }

        renderbmp.UnlockBits(&bitmapData);

        return true;
    }
private:
    Gdiplus::GdiplusStartupInput m_gdiplusStartupInput;
    ULONG_PTR m_gdiplusToken;
    Gdiplus::Bitmap* jpg1;
    Gdiplus::Bitmap* jpg2;
    Gdiplus::Graphics* g;
    Gdiplus::Graphics* g2;
    Gdiplus::Bitmap* bmp;
    Gdiplus::Bitmap* bmp2;
};

我可以使用这个 H264Writer 来处理 OpenGL 吗?

答案是肯定的,如果您不想使用更复杂的 OpenGL H264Writer(它需要 win32 线程同步)。OpenGL 和该 H264Writer 协同渲染/编码,如下所示。蓝色框中的数字是 OpenGL 中的帧号,而洋红色框中的数字是编码过程中的对应帧。在上面的图表中,假设 OpenGL 和编码帧需要相同的处理时间。OpenGL 填充帧缓冲区并发出信号给 win32 事件,然后视频编码线程接管。

2 threads

而在本文介绍的这个 H264Writer 中,如果您调用 OpenGL 填充帧缓冲区的 renderFunction,您将获得单线程性能,因为 OpenGL 和编码发生在同一个线程中。编码在底层可以使用多个线程。为了简单起见,我们假设编码使用 1 个线程。

Single threaded

出于性能原因,最好仅使用更复杂的 OpenGL H264Writer。代码托管在 GitHub。运行可执行文件之前,请记住将 image 文件夹复制到 DebugRelease 文件夹。祝您将精彩的动画转换为 H264/HEVC 视频,与他人分享并珍藏!

硬件加速

硬件加速现在已在库中可用,通过在属性中设置 MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS 并将其传递给 MFCreateSinkWriterFromURL

CComPtr<IMFAttributes> attrs;
MFCreateAttributes(&attrs, 1);
attrs->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE);

hr = MFCreateSinkWriterFromURL(m_DestFilename.c_str(), nullptr, attrs, &m_pSinkWriter);

当您运行不带 OpenGL 的视频编码器时,您应该会在 Windows 10 任务管理器中看到“GPU 1 - Video Encode”。使用 OpenGL 时,您会看到“GPU 1 - Copy”。

v0.4.2 版本构造函数中的质量参数

  • int numWorkerThreads: 0 表示使用默认值
  • int qualityVsSpeed: [0:100] 0 表示速度,100 表示质量
  • RateControlMode mode: 3 种模式可供选择 UnconstrainedVBRQualityCBRVBR 是可变比特率,CBR 是恒定比特率)
  • int quality: 仅在 mode 为 Quality 时有效。[0:100] 0 表示文件大小较小且质量较低,100 表示文件较大且质量较高

如何录制实时动画?

  • 创建一个 Win32 事件,让 Render() 等待该事件被触发。
  • 设置一个定时器定期运行以触发 Win32 事件。
  • pixels 的更新必须同步,您不希望像素同时被更新和读取,这会导致撕裂效果。
  • 注意:不要偷懒使用 Sleep(),因为我们不知道每次间隔之间应该睡眠多久,因为编码每一帧所需的时间可能不同。

使用此库的软件

历史

  • 2023 年 4 月 25 日:添加了一个关于使用此库的软件的章节。如果您有软件正在使用此视频编码库,请告知我,以便我将其添加到此列表中。
  • 2022 年 10 月 27 日:在引言中添加了 Mandy Frenzy 视频和链接
  • 2022 年 3 月 8 日:添加了一个关于如何录制实时动画?的章节
  • 2022 年 3 月 6 日:添加了 HasH264()HasHEVC() 来检查系统上是否存在硬件加速或软件编码器。请记住在 C++ 中调用 HasH264()HasHEVC() 之前调用 CoInitialize()。C# 应用程序默认调用 CoInitialize()。为在虚拟机中进行测试,恢复了软件编码选项。
    注意:选择软件编码时会忽略质量设置,因为它们不起作用。
  • 2022 年 2 月 17 日:添加了 HasHEVC() 以检查系统上是否存在硬件加速 HEVC 编码器。
  • 2020 年 8 月 9 日:上传了预编译的 C# DLL,供那些在构建 C++/CLI DLL 时遇到困难的人使用。您的 C# 应用程序模式必须是 x86 或 x64 才能使用 DLL。严格来说,不支持 AnyCPU 模式,该模式可以默认为 x86 或 x64。
  • 2020 年 7 月 26 日:添加了 0.4.2 版本以调整质量和编码速度参数(有关更多信息,请参阅质量部分
  • 2020 年 7 月 25 日:添加了 0.4.0 版本以支持硬件加速(有关更多信息,请参阅硬件加速部分
  • 2019 年 7 月 20 日:为 C# 使用添加了 C++/CLI 版本
  • 2019 年 7 月 19 日:添加了 OpenGL 部分
  • 2019年7月2日:初始版本

《将你的...》系列的其他文章

© . All rights reserved.