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






4.99/5 (45投票s)
使用 C++ 和 C# 通过硬件加速将您的动画转换为 H264/HEVC 视频
目录
- 引言
- 红色视频
- 单张 JPEG 视频
- 两张 JPEG 视频
- 文本动画视频
- 我可以使用这个 H264Writer 来处理 OpenGL 吗?
- 硬件加速
- v0.4.2 版本构造函数中的质量参数
- 如何录制实时动画?
- 使用此库的软件
- 历史
引言
我最近完成了我的(闭源)照片幻灯片应用程序 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
参数可以是 H264
或 HEVC
。width
和 height
参数指视频的宽度和高度。fps
参数是视频的每秒帧数,我通常指定为 30 或 60。duration
参数指视频时长(毫秒),可以设置为 -1
表示视频时长与 MP3 相同。bitrate
参数指每秒字节的视频比特率。请记住为高分辨率视频和 HEVC 设置更高的比特率。纯虚函数 FrameRenderer
的 Render
函数签名如下。width
和 height
是视频尺寸。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;
};
红色视频
对于我们的第一个示例,我们保持简单。我们只渲染一个红色视频。
这是 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
为零时。
现在我们使用 GDI+,因此我们必须包含 Gdiplus.h 头文件及其 Gdiplus.lib。frameRenderer
类型现在设置为 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
,因此我们跳过显示它。
Render2JPG
与 RenderJPG
几乎相同,只是它使用 Bitmap
类加载 2 张 JPEG 图片。当持续时间小于或等于 1000 毫秒时,存储在 alpha
变量中的透明度为零(完全透明),当持续时间大于或等于 2000 毫秒时,为 255(完全不透明)。在 1000 到 2000 毫秒的持续时间之间,会计算 alpha
。关于 frame_duration = 1000 / fps
的一个小提示:由于它是整数,因此不精确。例如,当 fps 为 30 时:1000/30 得到 33 毫秒,但 30 * 33 只会得到 990 毫秒,而不是原始的 1000 毫秒。使用 fps
和 frame_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
向下逐渐移动渲染的。jpg1
和 jpg2
是误称,因为加载的图像实际上是 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 事件,然后视频编码线程接管。
而在本文介绍的这个 H264Writer
中,如果您调用 OpenGL 填充帧缓冲区的 renderFunction
,您将获得单线程性能,因为 OpenGL 和编码发生在同一个线程中。编码在底层可以使用多个线程。为了简单起见,我们假设编码使用 1 个线程。
出于性能原因,最好仅使用更复杂的 OpenGL H264Writer。代码托管在 GitHub。运行可执行文件之前,请记住将 image 文件夹复制到 Debug 或 Release 文件夹。祝您将精彩的动画转换为 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 种模式可供选择UnconstrainedVBR
、Quality
、CBR
(VBR
是可变比特率,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日:初始版本