OpenGL 的 H264/HEVC 视频编码器





5.00/5 (18投票s)
用于录制 OpenGL 渲染的 H264/HEVC 视频编码器
虽然这篇长文比编码器库本身拥有更多的行数,但它是一篇非常简单易读易懂的文章。如果您之前读过我的其他文章,您会感到很舒服,因为我不会写复杂的东西。
目录
要求
视频编码器
- Visual C++ 2015/2017
- Windows 7/8/10
OpenGL 渲染器
- SDL2
- SDL2 Image
- GLEW
- GLM
- Tiny OBJ loader
- Zlib
所有 OpenGL 渲染器所需的库都包含在仓库中。重点在于视频编码器。
引言
我刚刚完成了我的(闭源)照片幻灯片应用程序 Mandy Frenzy,该应用程序基于本文中介绍的视频编码器。下面是从该应用程序编码的视频。您可以从这里下载我的幻灯片应用程序,或访问其主页。
我在编写我的 Windows 应用商店应用 Mandy Frenzy(一个面向女性的照片幻灯片应用)时,也在开发这个视频编码器。(更新:由于 WPF 的性能问题,Mandy Frenzy 已从 C# WPF 转换为 C++ MFC)。我现在感到筋疲力尽,所以暂时休息一下。在此期间,我正在写一系列短篇文章来记录这个应用。这个视频编码器是一个仅包含头文件(H264Writer.h)的库,基于 Microsoft Media Foundation,而不是旧的 DirectShow
,因为 Microsoft 没有在 DirectShow
中公开 H264 和 HEVC 编解码器。它已在 Windows 10 上进行测试。在 Windows 7/8 上应该也能正常工作。如果您在这些操作系统上遇到任何问题,请告知我。Windows 10 曾捆绑 HEVC 编解码器。但出于未知原因,Microsoft 在 Windows 10 Fall Creators Update 中已将其从**新**的 Windows 10 安装中移除,并在 Microsoft Store 中以 1.50 美元的价格出售。在下面的部分中,一些截图将展示 MS HEVC 视频中存在的编码伪影。 HEVC 质量问题已通过硬件加速修复。
此编码器与 FFmpeg 相比的优缺点是什么?
FFmpeg 是 GPL 许可的,所以如果您只是想编码个人渲染器的输出,您可能不必担心。对于我的免费增值应用,我更愿意避免许可问题。业余爱好者通常通过 FFmpeg
对帧进行编码的方式是先将所有帧保存到硬盘,这限制了帧的数量,并且还直接影响到视频可保存的时长,具体取决于可用硬盘空间。保存和打开文件的额外步骤会对编码速度产生负面影响。当然,与 FFmpeg
代码进行紧密集成可以消除帧保存的部分。另一方面,此编码器从提供的 framebuffer
读取 RGB 值。缺点是它不便携,仅在 Windows 7/8/10 上工作。
3 种渲染模式
同一个 OpenGL 渲染器可以编译成三种模式:普通 OpenGL 显示模式、视频编码器模式和 Emscripten 模式。后两种模式的代码段分别由 VIDEO_ENCODER
和 EMSCRIPTEN
宏进行保护。您可以自行使用自己的渲染器与视频编码器结合。默认的 OpenGL 渲染器仅用于演示。
文档分为三个主要部分。第一部分是关于如何设置演示并运行,以及如何修改参数。第二部分是关于如何将其集成到您的 OpenGL 框架中。该演示使用了 Paul Varcholik 的 OpenGL Essentials LiveLessons 中教授的渲染器框架。原始源代码使用了 GLFW 并基于 OpenGL 4.4:我将其框架转换为使用 SDL 并降级到 OpenGL 2.0。这样做的决定基于 WebGL 和 Emscripten 能支持的最低通用标准。稍后将有关于如何与 DirectX 集成的教程。理论上,此视频编码器应能很好地与其他图形 API(如 Vulkan)集成,毕竟它只需要一个视频缓冲区和一些同步即可完成工作。第三部分是关于视频编码器内部机制的解释,如果您不关心编码器内部实现,可以跳过。最后一部分解释了编译为 asm.js 或 Webassembly
所需的 Emscripten 部分。
运行演示
宇宙飞船视频
所有必需的库都包含在仓库中。所需的 DLL 文件在 Win32 的构建后会自动复制到 Release 或 Debug 文件夹。x64 构建由于无法在网上找到 x64 的 zlib 库/DLL 而无法构建;这是 OpenGL 渲染器的一个链接问题,而不是视频编码器的问题。
要查看 OpenGL 演示,请在 Visual Studio 中打开 SDL_App.sln 并构建 SDL_App
项目。
要运行视频编码演示,请打开 H264SinkWriter.cpp,并在 main 函数中,将 configFile
、musicFile
和 videoFile
修改为您机器上的路径。configFile
位于 $(SolutionDir)SDL_App 文件夹中。musicFile
应该是 mp3 文件,如果留空,则最终视频将没有音乐。videoFile
是编码后的视频。您可以指定 HVEC(即 H265)而不是 H264 作为第 4 个参数。HVEC 存在一些编码问题,颜色会溢出(请参阅下面的截图)。
// This is the config file to be found in SDL_App project folder.
std::wstring configFile
(L"D:\\GitHub\\video_encoder_for_ogl_dx\\SDL_App\\SDL_App\\config.txt");
// This is your music file
std::wstring musicFile(L"D:\\FMA.mp3");
// This is the video encoded output file.
std::wstring videoFile(L"C:\\Users\\shaov\\Documents\\video.mp4");
H264Writer writer(musicFile.c_str(), configFile.c_str(),
videoFile.c_str(), VideoCodec::H264);
if (writer.IsValid())
{
if (writer.Process())
{
printf("Video written successfully!\n");
return 0;
}
}
printf("Video write failed!\n");
getchar();
典型的 config.txt 用于将信息传递给 OpenGL 渲染器,与视频编码器无关。如果您的渲染器能够获取有关即将编码的视频的信息,则只需传递一个虚拟的 config.txt。典型的 config.txt 内容如下:
ScreenWidth=800
ScreenHeight=600
LogPath=C:\Users\shaov\Documents\log.txt
FPS=60
现在演示不自动处理宽高比,始终固定为 4:3。如果您在屏幕宽度和高度中输入任何 16:9 或宽于 4:3 的值,您的视频看起来会拉伸。FPS
条目用于每秒帧数(整数),无法输入像 29.7777
这样的十进制数。
演示仅编码 5 秒视频。在 RenderingScene::Draw
函数中更改时长。
void RenderingScene::Draw(const SceneTime& gameTime)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
float grey = 35.0f / 255.0f;
glClearColor(grey, grey, grey, 1.0f);
Scene::Draw(gameTime);
SDL_GL_SwapWindow(mWindow);
#ifdef VIDEO_ENCODER
static GLfloat start_time = gameTime.TotalGameTime();
GLfloat elapsed_time = gameTime.TotalGameTime() - start_time;
// During video encoding, only run for 5 seconds.
if(elapsed_time > 5.0f)
{
Scene::setVideoEnded();
}
#endif
}
您需要进行实验才能找到编码高质量视频的最佳比特率。
enum class VideoCodec
{
H264,
HVEC
};
// H264Writer constructor
H264Writer(const wchar_t* mp3_file, const wchar_t* src_file, const wchar_t* dest_file,
VideoCodec codec, UINT32 bitrate = 4000000) :
{...}
要独立运行演示可执行文件,您需要将 config.txt、Images 和 Models 文件夹复制到 Release/Debug 文件夹。SDL2 DLL 会在构建后复制。
默认情况下,演示显示一个旋转的 UFO 飞碟,要显示其他 3D 模型,只需取消注释 CreateComponents()
中的其他行。
void RenderingScene::CreateComponents()
{
mCamera = std::unique_ptr<FirstPersonCamera>(new
FirstPersonCamera(*this, 45.0f, 1.0f / 0.75f, 0.01f, 100.0f));
mComponents.push_back(mCamera.get());
mServices.AddService(Camera::TypeIdClass(), mCamera.get());
try
{
//mTexturedModelDemo = std::unique_ptr<TexturedDemo>
//(new TexturedDemo(*this, *mCamera));
//mComponents.push_back(mTexturedModelDemo.get());
//mDiffuseCube = std::unique_ptr<DiffuseCube>(new DiffuseCube(*this, *mCamera,
// "Cube.obj.txt", "Cube.mtl.txt"));
//mComponents.push_back(mDiffuseCube.get());
mUFOSpecularModel = std::unique_ptr<SpecularModel>
(new SpecularModel(*this, *mCamera,
"UFOSaucer3.jpg", "UFOSaucer.obj.txt.zip", "UFOSaucer.mtl.txt"));
mComponents.push_back(mUFOSpecularModel.get());
//mStarModel = std::unique_ptr<StarModel>(new StarModel(*this, *mCamera,
// glm::vec3(1.0f,1.0f,0.0f), "Star.obj.txt", "Star.mtl.txt"));
//mComponents.push_back(mStarModel.get());
}
catch (SceneException& e)
{
char buf[1000];
SPRINTF(buf, "ModelEffect exception:%s", e.what());
gLogger.DebugPrint(buf);
}
#ifdef __EMSCRIPTEN__
gDownloadSingleton.DownloadAllNow();
#endif
}
HEVC 伪影
Mandy Frenzy 的 H264 视频
Mandy Frenzy 的 HEVC 视频(有伪影)
更新:HEVC 质量问题已通过硬件加速解决。
正如您所见,HEVC 中存在正弦波伪影,而 H264 中不存在。增加比特率可以解决问题,但文件大小会增加。顺便说一句,正弦波是由三角形而不是线条渲染的,也不是由片段着色器渲染的。原因是线条在 OpenGL ES 2.0 中通常实现为 1 像素宽。使用三角形可以让我控制宽度/高度。
与您的 OpenGL 框架集成
本节介绍将视频编码器集成到您的渲染器中所需的修改。
查找所有编码相关代码的最快方法是在源代码中搜索 VIDEO_ENCODER
宏。编码器要求您实现两个全局函数:check_config_file
和 encoder_start
。请参阅下面的声明。encoder_start
在工作线程中被调用。
extern int check_config_file(const wchar_t* file, int* width, int* height, int* fps);
extern int encoder_start(UINT** pixels, HANDLE evtRequest, HANDLE evtReply,
HANDLE evtExit, HANDLE evtVideoEnded, const WCHAR* szUrl);
它们反过来调用 Program.cpp 中实现的 DLL 对应函数。
int check_config_file(const wchar_t* file, int* width, int* height, int* fps)
{
return ::check_project_file(file, width, height, fps);
}
int encoder_start(UINT** pixels, HANDLE evtRequest, HANDLE evtReply,
HANDLE evtExit, HANDLE evtVideoEnded, const WCHAR* szUrl)
{
return ::encoder_main(pixels, evtRequest, evtReply, evtExit,
evtVideoEnded, szUrl);
}
check_project_file
要求您从文件(即 config.txt)中传入屏幕 width
、height
和 FPS
信息。实际上,您可以打开任何可以为您提供此信息的文档,因此 check_project_file
的实现并不重要。让我们看看 encoder_main
是如何实现的。
SDL_APP_DLL_API int WINAPI check_project_file(const wchar_t* file, int* width,
int* height, int* fps)
{
ConfigSingleton config;
const std::string afile = toAString(file);
bool ret = config.LoadConfigFile(afile);
if (ret)
{
*width = config.GetScreenWidth();
*height = config.GetScreenHeight();
*fps = config.GetFPS();
}
return ret;
}
SDL_APP_DLL_API int WINAPI encoder_main(UINT** pixels, HANDLE evtRequest,
HANDLE evtReply, HANDLE evtExit,
HANDLE evtVideoEnded, const WCHAR* szUrl)
{
try
{
const std::string& config_file = toAString(szUrl);
gConfigSingleton.OpenFile(config_file.c_str(),
Library::DownloadableComponent::FileType::INI_FILE);
if (gConfigSingleton.IsLoadSuccess() == false)
{
gLogger.Log("gConfigSingleton.IsLoadSuccess() failed! See log!");
return 1;
}
Texture::setScreenDim(gConfigSingleton.GetScreenWidth(),
gConfigSingleton.GetScreenHeight());
std::unique_ptr<RenderingScene>
renderingScene(new RenderingScene(L"Photo Montage",
gConfigSingleton.GetScreenWidth(), gConfigSingleton.GetScreenHeight()));
renderingScene->initVideoEncoder(pixels, evtRequest, evtReply, evtExit,
evtVideoEnded, gConfigSingleton.GetFPS());
renderingScene->Run();
}
catch (SceneException& ex)
{
gLogger.Log(ex.GetError().c_str());
}
catch (std::exception& ex)
{
gLogger.Log(ex.what());
}
return 0;
}
encoder_main
非常类似于 WinMain
,只是它调用 initVideoEncoder
函数来传递参数。这是 initVideoEncoder
的实现方式:它只是将某些成员归零初始化,并将参数保存在其成员中。这些 HANDLE
参数已在 H264Writer
构造函数中初始化。setTimeQuandant()
用于设置每个帧的时间增量,例如,如果 FPS
是 30
,则时间增量应为 33.3
,而不管实际经过的时间。您不希望视频编码具有可变的时间速率。
void Scene::initVideoEncoder(UINT** pixels, HANDLE evtRequest, HANDLE evtReply,
HANDLE evtExit, HANDLE evtVideoEnded, int fps)
{
mTexture = 0;
mRenderBuffer = 0;
mDepthBuffer = 0;
mMultisampleTexture = 0;
mPixels = pixels;
mPixelBuffer = nullptr;
mEvtRequest = evtRequest;
mEvtReply = evtReply;
mEvtExit = evtExit;
mEvtVideoEnded = evtVideoEnded;
mGameClock.setTimeQuandant((1000.0f/(float)fps)/1000.0f);
}
这就是 encoder_start
如何在由 H264Writer
启动的工作线程中被调用的。
DWORD WINAPI ThreadOpenGLProc(LPVOID pParam)
{
H264Writer* pWriter = (H264Writer*)(pParam);
return ::encoder_start((UINT**)(
pWriter->GetImagePtr()),
pWriter->GetRequestEvent(),
pWriter->GetReplyEvent(),
pWriter->GetExitEvent(),
pWriter->GetVideoEndedEvent(),
pWriter->GetUrl().c_str());
}
在我们的 OpenGL
渲染器中,我们需要为渲染器创建一个 RenderBuffer
来进行绘制。
void Scene::CreateFBO()
{
#ifdef VIDEO_ENCODER
mPixelBuffer = new unsigned int[mScreenWidth*mScreenHeight];
glGenTextures(1, &mMultisampleTexture);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, mMultisampleTexture);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 4,
GL_RGBA, mScreenWidth, mScreenHeight, GL_TRUE);
glGenRenderbuffers(1, &mRenderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, mRenderBuffer);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 16, GL_RGBA8,
mScreenWidth, mScreenHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER, mRenderBuffer);
// Create depth render buffer (This is optional)
glGenRenderbuffers(1, &mDepthBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, mDepthBuffer);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 16,
GL_DEPTH24_STENCIL8, mScreenWidth, mScreenHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_RENDERBUFFER, mDepthBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT,
GL_RENDERBUFFER, mDepthBuffer);
GLuint mTexture = 0;
glGenTextures(1, &mTexture);
glBindTexture(GL_TEXTURE_2D, mTexture);
unsigned int dim = determineMinDim(mScreenWidth, mScreenHeight);
unsigned int* pixels = new unsigned int[dim * dim];
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, dim, dim, 0,
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
delete [] pixels;
pixels = NULL;
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, mTexture, 0);
// Enable multisampling
glEnable(GL_MULTISAMPLE);
if (GL_FRAMEBUFFER_COMPLETE == glCheckFramebufferStatus(GL_FRAMEBUFFER))
{
OutputDebugStringA("FBO status: Complete!\n");
}
else
{
OutputDebugStringA("FBO status: Not complete!\n");
}
#endif // VIDEO_ENCODER
}
上面函数中使用的 determineMinDim()
实现方式是,我们需要一个 2 的幂次的方形纹理。
#ifdef VIDEO_ENCODER
unsigned int Scene::determineMinDim(unsigned int width, unsigned int height)
{
unsigned int dim = width;
if (height > width)
dim = height;
unsigned int min_dim = 32;
if (dim > 32 && dim <= 64)
min_dim = 64;
else if (dim > 64 && dim <= 128)
min_dim = 128;
else if (dim > 128 && dim <= 256)
min_dim = 256;
else if (dim > 256 && dim <= 512)
min_dim = 512;
else if (dim > 512 && dim <= 1024)
min_dim = 1024;
else if (dim > 1024 && dim <= 2048)
min_dim = 2048;
else if (dim > 2048 && dim <= 4096)
min_dim = 4096;
else
min_dim = 4096;
return min_dim;
}
#endif // VIDEO_ENCODER
最后,我们需要使用 glReadPixels
读取渲染后的缓冲区。代码等待 mEvtRequest
和 mEvtExit
。当编码线程请求新帧时,mEvtRequest
会被触发。而在退出时,mEvtExit
会被触发。将缓冲区复制到 mPixels
后,mEvtReply
会被触发,告知编码线程 mPixels
已准备好。
void Scene::ReadBuffer(bool& quit)
{
#ifdef VIDEO_ENCODER
glReadBuffer(GL_COLOR_ATTACHMENT0);
glReadPixels(0, 0, mScreenWidth, mScreenHeight,
GL_BGRA_EXT, GL_UNSIGNED_BYTE, mPixelBuffer);
HANDLE arrHandle[2];
arrHandle[0] = mEvtRequest;
arrHandle[1] = mEvtExit;
DWORD dwEvt = WaitForMultipleObjects(2, arrHandle, FALSE, INFINITE);
if (dwEvt == WAIT_OBJECT_0 + 1)
{
quit = true;
}
while (*mPixels == NULL) { Sleep(100); }
if(*mPixels)
memcpy(*mPixels, mPixelBuffer, mScreenWidth*mScreenHeight*sizeof(unsigned int));
SetEvent(mEvtReply);
#endif // VIDEO_ENCODER
}
这就是我的 Render()
中如何调用 CreateFBO()
和 ReadBuffer()
。当不编译在 VIDEO_ENCODER
模式下时,CreateFBO()
和 ReadBuffer()
是空的。
void Scene::Render(bool& quit)
{
static bool all_init = false;
if (all_init == false)
{
if (IsAllReady())
{
CreateFBO();
Initialize();
if (IsAllInitialized())
{
all_init = true;
mGameClock.Reset();
}
}
}
if (all_init)
{
mGameClock.SetPause(mPaused);
if (mPaused == false)
{
mGameClock.UpdateGameTime(mGameTime);
}
Update(mGameTime);
Draw(mGameTime);
ReadBuffer(quit);
}
}
视频编码器是如何编写的
Media Foundation 提供了三种编写视频编码器的方法
- Media Session
- Transcoder API
SinkWriter
所有这些方法都有其优缺点。它们从最难/控制最多到最易用但控制最少。
第一种方法是构建您自己的媒体会话,这是一种拓扑结构。这种方法具有最大的灵活性/控制力,但难度最高,专为 MF 专业人士准备,因为您必须找到 Media Source、Transform 和 Media Sink 并将它们连接起来。但是,您有权灵活选择您的转换器供应商。对于不熟悉的人来说:Media Sources 用于解复用源文件。Media Foundation Transforms 用于解码和编码流。Media Sinks 用于复用流并将复用后的流写入文件或网络。使用此方法有一个缺点,因为您必须编写并注册您的 Media Source DLL 在用户计算机上。在大多数情况下,这不成问题。但是我的 UWP 应用的安装和运行是沙箱化的,这意味着所有文件保存和注册表写入都被重定向到未知位置。我不确定 MF 是否能找到并实例化我的 Media Source DLL,如果它只查看全局注册表和文件夹。
第二种方法是使用转码 API,它会为您构建媒体会话,并为您选择最合理的选项,从而使构建过程更加容易。这种方法对我来说不适用,因为我不是将视频文件转码成另一种格式。
第三种方法,SinkWriter
,是与您的 OpenGL
渲染器集成最简单的方法。一个非常大的缺点是它总是选择 Microsoft 的**软件**转换器,即使您的系统上有硬件加速的转换器可用,就像我的情况一样,我拥有 Intel 和 NVidia 的 H264 h/w 编码器(见下文)。 这是我选择的方法。我被这本书误导了:在最新的 1.0.3 版本中,第三种方法也可以利用硬件加速。
H/w encoder
Video Encoder: Intel Quick Sync Video H.264 Encoder MFT
Video Encoder: NVIDIA H.264 Encoder MFT
S/w encoder
Video Encoder: H264 Encoder MFT
使用 EnumVideoEncoder
函数(如下所示)可以枚举编码器。
std::vector<std::wstring> encoders;
if (H264Writer::EnumVideoEncoder(encoders, Processing::Software, VideoCodec::H264))
{
for (size_t i = 0; i < encoders.size(); ++i)
{
printf("Video Encoder: %S\n", encoders[i].c_str());
}
}
else
{
printf("H264Writer::EnumVideoEncoder failed!\n");
}
将继续研究第一种方法以利用 H/w 编码器。
要使用这个仅头文件的库,只需在您的 C++ 源代码中包含 H264Writer.h。并记住实现下面的两个函数。file
可以是任何格式。check_config_file
应返回帧尺寸和每秒帧率。
在上面的集成部分,有一个关于 encoder_start()
如何被 encoder_main()
实现的例子。
extern int check_config_file(const wchar_t* file, int* width, int* height, int* fps);
extern int encoder_start(UINT** pixels, HANDLE evtRequest,
HANDLE evtReply, HANDLE evtExit, HANDLE evtVideoEnded, const WCHAR* szUrl);
在 H264Writer
的构造函数中,正如您所见,它将一组成员初始化为默认值。其中 m_hThread
运行 OpenGL Win32 循环。此编码器的一个特点是 OpenGL 窗口在前台运行。用户将看到该窗口。如果您的应用程序不需要向用户显示窗口,您必须在 encoder_main()
中创建一个不可见的窗口。m_evtRequest
用于从 OpenGL
线程请求帧,m_evtReply
用于 OpenGL
通知帧已复制并准备就绪。如果在视频编码完成前用户关闭窗口,则会触发 m_evtExit
。m_evtVideoEnded
由 Scene::setVideoEnded()
在 Draw()
函数中触发。
H264Writer(const wchar_t* mp3_file, const wchar_t* src_file,
const wchar_t* dest_file, VideoCodec codec, UINT32 bitrate = 4000000) :
m_OpenSrcFileSuccess(false),
m_MP3Filename(mp3_file),
m_SrcFilename(src_file),
m_DestFilename(dest_file),
m_Width(0),
m_Height(0),
m_pImage(nullptr),
m_cbWidth(4 * m_Width),
m_cbBuffer(m_cbWidth * m_Height),
m_pBuffer(nullptr),
m_hThread(INVALID_HANDLE_VALUE),
m_evtRequest(INVALID_HANDLE_VALUE),
m_evtReply(INVALID_HANDLE_VALUE),
m_evtExit(INVALID_HANDLE_VALUE),
m_evtVideoEnded(INVALID_HANDLE_VALUE),
m_CoInited(false),
m_MFInited(false),
m_pSinkWriter(nullptr),
m_VideoFPS(60),
m_FrameDuration(10 * 1000 * 1000 / m_VideoFPS),
m_VideoBitrate(bitrate),
m_EncCommonQuality(100),
m_VideoCodec(codec),
m_nStreams(0)
{
在构造函数体内部,调用 check_config_file
来返回 width
、height
和 fps
。
int width = 0; int height = 0; int fps = 0;
if (check_config_file(m_SrcFilename.c_str(), &width, &height, &fps))
{
m_OpenSrcFileSuccess = true;
m_Width = width;
m_Height = height;
m_cbWidth = 4 * m_Width;
m_cbBuffer = m_cbWidth * m_Height;
m_VideoFPS = fps;
m_FrameDuration = (10 * 1000 * 1000 / m_VideoFPS);
m_pImage = new (std::nothrow) UINT32[m_Width * m_Height];
}
if (!m_OpenSrcFileSuccess)
return;
if (!m_pImage)
return;
接下来,调用 CoInitializeEx
来初始化 COM 运行时,然后才能调用任何 Media Foundation 函数。调用 MFStartup()
来初始化 Media Foundation (MF) 运行时。之后,我们根据存储在 m_cbBuffer
中的尺寸创建 m_pBuffer
。这是过早优化的经典案例。缓冲区保存在成员中,以避免在每一帧上实例化和分配内存缓冲区。事实证明,MFCreateMemoryBuffer
并不在每一帧中销毁缓冲区,而是将缓冲区返回到池中;每当请求具有相同尺寸的缓冲区时,MF 就会为您提供一个之前创建的缓冲区。
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if (SUCCEEDED(hr))
{
m_CoInited = true;
hr = MFStartup(MF_VERSION);
if (SUCCEEDED(hr))
{
m_MFInited = true;
// Create a new memory buffer.
hr = MFCreateMemoryBuffer(m_cbBuffer, &m_pBuffer);
}
}
在 IsValid()
返回 true
之后,我们创建 Win32
事件句柄和 OpenGL
线程。
if (IsValid() == false)
{
return;
}
if (SUCCEEDED(hr))
{
m_evtRequest = CreateEvent(NULL, FALSE, FALSE, NULL);
m_evtReply = CreateEvent(NULL, FALSE, FALSE, NULL);
m_evtExit = CreateEvent(NULL, FALSE, FALSE, NULL);
m_evtVideoEnded = CreateEvent(NULL, FALSE, FALSE, NULL);
m_hThread = CreateThread(NULL, 100000, ThreadOpenGLProc, this, 0, NULL);
}
}
有效性取决于 COM 和 MF 运行时是否初始化成功。
const bool IsValid() const
{
return m_CoInited && m_MFInited;
}
在 H264Writer
的析构函数中,触发 m_evtExit
并休眠 100 毫秒,以确保 OpenGL
线程接收到事件。然后可以安全地释放所有缓冲区和句柄。并关闭 MF 和 COM 运行时。
~H264Writer()
{
SetEvent(m_evtExit);
Sleep(100);
if (m_pImage)
{
delete[] m_pImage;
m_pImage = nullptr;
}
m_pBuffer.Release();
if (m_evtRequest != INVALID_HANDLE_VALUE)
CloseHandle(m_evtRequest);
if(m_evtReply != INVALID_HANDLE_VALUE)
CloseHandle(m_evtReply);
if(m_evtExit != INVALID_HANDLE_VALUE)
CloseHandle(m_evtExit);
if(m_evtVideoEnded!=INVALID_HANDLE_VALUE)
CloseHandle(m_evtVideoEnded);
m_pSourceReader = nullptr;
m_pSinkWriter = nullptr;
if(m_MFInited)
MFShutdown();
if(m_CoInited)
CoUninitialize();
}
GetSourceDuration
在 H264Writer
中用于获取源 MP3 的时长,虽然它可以用于任何媒体(包括视频)的时长。时长以千万分之一秒为单位。
HRESULT GetSourceDuration(IMFMediaSource *pSource, MFTIME *pDuration)
{
*pDuration = 0;
IMFPresentationDescriptor *pPD = NULL;
HRESULT hr = pSource->CreatePresentationDescriptor(&pPD);
if (SUCCEEDED(hr))
{
hr = pPD->GetUINT64(MF_PD_DURATION, (UINT64*)pDuration);
pPD->Release();
}
return hr;
}
这是一个将 duration
转换回人类熟悉的分钟和秒的简短片段。
MFTIME total_seconds = duration / 10000000;
MFTIME minute = total_seconds / 60;
MFTIME second = total_seconds % 60;
下一个函数是 InitializeWriter()
。
HRESULT InitializeWriter(DWORD *videoStreamIndex, DWORD *audioStreamIndex)
{
HRESULT hr = S_OK;
m_pSourceReader = nullptr;
m_pSinkWriter = nullptr;
*videoStreamIndex = 1;
*audioStreamIndex = 0;
IMFMediaType *pMediaTypeOut = nullptr;
IMFMediaType *pMediaTypeIn = nullptr;
DWORD streamIndex=1;
CComPtr<IMFAttributes> pConfigAttrs;
我们设置了首选硬件加速转换的属性。当系统上没有安装硬件转换器时,将选择软件转换器。
do
{
// create an attribute store
hr = MFCreateAttributes(&pConfigAttrs, 1);
BREAK_ON_FAIL(hr);
// set MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS property in the store
hr = pConfigAttrs->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE);
BREAK_ON_FAIL(hr);
当指定了 m_MP3Filename
时,我们创建一个能够解码 mp3 的 m_pSourceReader
。pSource
用于获取和打印时长,这没有用。所以你可以删除 pSource
行以及它下面的所有行。
if (m_MP3Filename.empty() == false)
{
// create a source reader
hr = MFCreateSourceReaderFromURL(m_MP3Filename.c_str(),
pConfigAttrs, &m_pSourceReader);
BREAK_ON_FAIL(hr);
IMFMediaSource* pSource = nullptr;
hr = m_pSourceReader->GetServiceForStream(
MF_SOURCE_READER_MEDIASOURCE,
GUID_NULL, //MF_MEDIASOURCE_SERVICE,
IID_IMFMediaSource,
(void**)&pSource
);
BREAK_ON_FAIL(hr);
MFTIME duration = 0;
hr = GetSourceDuration(pSource, &duration);
BREAK_ON_FAIL(hr);
if (pSource)
pSource->Release();
printf("Audio duration:%lld\n", duration);
MFTIME total_seconds = duration / 10000000;
MFTIME minute = total_seconds / 60;
MFTIME second = total_seconds % 60;
printf("Audio duration:%lld:%lld\n", minute, second);
}
如您所见,我们的 do-while
只执行了一次。do-while
的存在主要是为了让 BREAK_ON_FAIL
宏能够从中跳出。接下来,我们创建一个 SinkWriter
。MapStreams()
、SetVideoOutputType()
和 SetVideoInputType()
将稍后解释。
hr = MFCreateSinkWriterFromURL(m_DestFilename.c_str(),
nullptr, nullptr, &m_pSinkWriter);
BREAK_ON_FAIL(hr);
// map the streams found in the source file from the source reader to the
// sink writer, while negotiating media types
hr = MapStreams();
BREAK_ON_FAIL(hr);
hr = SetVideoOutputType(&pMediaTypeOut, streamIndex);
BREAK_ON_FAIL(hr);
hr = SetVideoInputType(&pMediaTypeIn, streamIndex);
BREAK_ON_FAIL(hr);
hr = m_pSinkWriter->BeginWriting();
BREAK_ON_FAIL(hr);
*videoStreamIndex = streamIndex;
} while (false);
SafeRelease(&pMediaTypeOut);
SafeRelease(&pMediaTypeIn);
return hr;
}
BREAK_ON_FAIL
和 BREAK_ON_NULL
宏定义如下。
#define BREAK_ON_FAIL(value) if(FAILED(value)) break;
#define BREAK_ON_NULL(value, newHr) if(value == NULL) { hr = newHr; break; }
SetVideoOutputType()
用于设置视频输出的参数。您可以尝试不同的值,看看它们如何影响输出。至于要设置哪些参数,Google 是您最好的朋友。我将其设置为最高质量。
HRESULT SetVideoOutputType(IMFMediaType** pMediaTypeOut, DWORD& streamIndex)
{
HRESULT hr = S_OK;
do
{
hr = MFCreateMediaType(pMediaTypeOut);
BREAK_ON_FAIL(hr);
hr = (*pMediaTypeOut)->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
BREAK_ON_FAIL(hr);
if (m_VideoCodec==VideoCodec::HEVC)
{
hr = (*pMediaTypeOut)->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_HEVC);
BREAK_ON_FAIL(hr);
hr = (*pMediaTypeOut)->SetUINT32(MF_MT_MPEG2_PROFILE,
eAVEncH265VProfile_Main_420_8);
BREAK_ON_FAIL(hr);
}
else
{
hr = (*pMediaTypeOut)->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264);
BREAK_ON_FAIL(hr);
hr = (*pMediaTypeOut)->SetUINT32(MF_MT_MPEG2_PROFILE, eAVEncH264VProfile_High);
BREAK_ON_FAIL(hr);
}
BREAK_ON_FAIL(hr);
hr = (*pMediaTypeOut)->SetUINT32(MF_MT_AVG_BITRATE, m_VideoBitrate);
BREAK_ON_FAIL(hr);
hr = (*pMediaTypeOut)->SetUINT32
(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
BREAK_ON_FAIL(hr);
hr = MFSetAttributeSize((*pMediaTypeOut), MF_MT_FRAME_SIZE, m_Width, m_Height);
BREAK_ON_FAIL(hr);
hr = MFSetAttributeRatio((*pMediaTypeOut), MF_MT_FRAME_RATE, m_VideoFPS, 1);
BREAK_ON_FAIL(hr);
hr = MFSetAttributeRatio((*pMediaTypeOut), MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
BREAK_ON_FAIL(hr);
hr = m_pSinkWriter->AddStream((*pMediaTypeOut), &streamIndex);
} while (false);
return hr;
}
SetVideoInputType()
用于设置输入视频格式,我们的 OpenGL 格式是 RGB32
。
const GUID VIDEO_ENCODING_FORMAT = MFVideoFormat_H264;
const GUID VIDEO_INPUT_FORMAT = MFVideoFormat_RGB32;
HRESULT SetVideoInputType(IMFMediaType** pMediaTypeIn, DWORD& streamIndex)
{
HRESULT hr = S_OK;
do
{
hr = MFCreateMediaType(pMediaTypeIn);
BREAK_ON_FAIL(hr);
hr = (*pMediaTypeIn)->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
BREAK_ON_FAIL(hr);
hr = (*pMediaTypeIn)->SetGUID(MF_MT_SUBTYPE, VIDEO_INPUT_FORMAT);
BREAK_ON_FAIL(hr);
hr = (*pMediaTypeIn)->SetUINT32
(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
BREAK_ON_FAIL(hr);
hr = MFSetAttributeSize(*pMediaTypeIn, MF_MT_FRAME_SIZE, m_Width, m_Height);
BREAK_ON_FAIL(hr);
hr = MFSetAttributeRatio(*pMediaTypeIn, MF_MT_FRAME_RATE, m_VideoFPS, 1);
BREAK_ON_FAIL(hr);
hr = MFSetAttributeRatio(*pMediaTypeIn, MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
BREAK_ON_FAIL(hr);
hr = m_pSinkWriter->SetInputMediaType(streamIndex, *pMediaTypeIn, NULL);
} while (false);
return hr;
}
ConnectStream()
尝试连接输入和输出,以便源和接收器能够达成一致。此函数主要是样板代码。
//
// Attempt to find an uncompressed media type for the specified stream
// that both the source
// and sink can agree on
//
HRESULT ConnectStream(DWORD dwStreamIndex,
const GUID& streamMajorType)
{
HRESULT hr = S_OK;
CComPtr<IMFMediaType> pPartialMediaType;
CComPtr<IMFMediaType> pFullMediaType;
BOOL fConfigured = FALSE;
GUID* intermediateFormats = NULL;
int nFormats = 0;
这里是熟悉的只执行一次的 do-while
循环。我们设置容器类型的 GUID
。
do
{
// create a media type container object that will be used to match stream input
// and output media types
hr = MFCreateMediaType(&pPartialMediaType);
BREAK_ON_FAIL(hr);
// set the major type of the partial match media type container
hr = pPartialMediaType->SetGUID(MF_MT_MAJOR_TYPE, streamMajorType);
BREAK_ON_FAIL(hr);
// Get the appropriate list of intermediate formats - formats that
// every decoder and encoder of that type should agree on.
// Essentially these are the uncompressed
// formats that correspond to decoded frames for video, and uncompressed audio
// formats
if (streamMajorType == MFMediaType_Video)
{
intermediateFormats = intermediateVideoFormats;
nFormats = nIntermediateVideoFormats;
}
else if (streamMajorType == MFMediaType_Audio)
{
intermediateFormats = intermediateAudioFormats;
nFormats = nIntermediateAudioFormats;
}
else
{
hr = E_UNEXPECTED;
break;
}
接下来,我们遍历每种格式,并使用 SetCurrentMediaType()
查找一种与 m_pSourceReader
匹配的格式。当它满意时,我们可以使用 GetCurrentMediaType
来获取完整的媒体类型。如果没有匹配的,我们将 hr
设置为 MF_E_INVALIDMEDIATYPE;
。
// loop through every intermediate format that you have for this major type, and
// try to find one on which both the source stream and sink stream can agree on
for (int x = 0; x < nFormats; x++)
{
// set the format of the partial media type
hr = pPartialMediaType->SetGUID(MF_MT_SUBTYPE, intermediateFormats[x]);
BREAK_ON_FAIL(hr);
// set the partial media type on the source stream
hr = m_pSourceReader->SetCurrentMediaType(
dwStreamIndex, // stream index
NULL, // reserved - always NULL
pPartialMediaType); // media type to try to set
// if the source stream
// (i.e. the decoder)
// is not happy with this media type -
// if it cannot decode the data into
// this media type,
// restart the loop in order
// to try the next format on the list
if (FAILED(hr))
{
hr = S_OK;
continue;
}
pFullMediaType = NULL;
// if you got here, the source stream is happy with the
// partial media type you set
// - extract the full media type for this stream (with all internal fields
// filled in)
hr = m_pSourceReader->GetCurrentMediaType(dwStreamIndex, &pFullMediaType);
// Now try to match the full media type to the corresponding sink stream
hr = m_pSinkWriter->SetInputMediaType(
dwStreamIndex, // stream index
pFullMediaType, // media type to match
NULL); // configuration attributes for the encoder
// if the sink stream cannot accept this
// media type - i.e., if no encoder was
// found that would accept this media
// type - restart the loop and try the next
// format on the list
if (FAILED(hr))
{
hr = S_OK;
continue;
}
// you found a media type that both the source and sink
// could agree on - no need
// to try any other formats
fConfigured = TRUE;
break;
}
BREAK_ON_FAIL(hr);
// if you didn't match any formats return an error code
if (!fConfigured)
{
hr = MF_E_INVALIDMEDIATYPE;
break;
}
} while (false);
return hr;
}
MapStreams()
用于 MP3 文件,所以当它未指定时,我们返回。m_pSourceReader
用于 MP3 文件。OpenGL 源不需要 SourceReader
,因为它未压缩,并且我们知道其格式,而且对于 OpenGL 输入没有视频文件可读,除非您计算包含尺寸和 FPS 的 config.txt。MapStreams()
大部分也是我从别处复制的样板代码。
HRESULT MapStreams(void)
{
if (m_MP3Filename.empty())
return S_OK;
HRESULT hr = S_OK;
BOOL isStreamSelected = FALSE;
DWORD sourceStreamIndex = 0;
DWORD sinkStreamIndex = 0;
GUID streamMajorType;
CComPtr<IMFMediaType> pStreamMediaType;
下面的代码将音频流添加到 sinkwriter。像视频这样的媒体文件可以包含比视频和音频更多的数据,例如 字幕
。对于编码器,我们不关心除 MFMediaType_Audio
之外的媒体类型。在代码中忽略 MFMediaType_Video
,因为 MapStreams
是为 MP3 m_pSourceReader
调用。
do
{
m_nStreams = 0;
while (SUCCEEDED(hr))
{
// check whether you have a stream with the right index - if you don't,
// the IMFSourceReader::GetStreamSelection() function will fail,
// and you will drop out of the while loop
hr = m_pSourceReader->GetStreamSelection
(sourceStreamIndex, &isStreamSelected);
if (FAILED(hr))
{
hr = S_OK;
break;
}
// count the total number of streams for later
m_nStreams++;
// get the source media type of the stream
hr = m_pSourceReader->GetNativeMediaType(
sourceStreamIndex, // index of the stream
// you are interested in
0, // index of the media type exposed by the
// stream decoder
&pStreamMediaType); // media type
BREAK_ON_FAIL(hr);
// extract the major type of the source stream from the media type
hr = pStreamMediaType->GetMajorType(&streamMajorType);
BREAK_ON_FAIL(hr);
// select a stream, indicating that the source should
// send out its data instead
// of dropping all of the samples
hr = m_pSourceReader->SetStreamSelection(sourceStreamIndex, TRUE);
BREAK_ON_FAIL(hr);
// if this is a video or audio stream, transcode it and
// negotiate the media type between the source reader stream
// and the corresponding sink writer stream.
// If this is a some other stream format (e.g. subtitles),
// just pass the media
// type unchanged.
if (streamMajorType == MFMediaType_Audio ||
streamMajorType == MFMediaType_Video)
{
// get the target media type - the media type into
// which you will transcode
// the data of the current source stream
hr = GetTranscodeMediaType(pStreamMediaType);
BREAK_ON_FAIL(hr);
// add the stream to the sink writer - i.e., tell the sink writer that
// a stream with the specified index will have the target media type
hr = m_pSinkWriter->AddStream(pStreamMediaType, &sinkStreamIndex);
BREAK_ON_FAIL(hr);
// hook up the source and sink streams - i.e. get them to agree on an
// intermediate media type that will be used to pass data between source
// and sink
hr = ConnectStream(sourceStreamIndex, streamMajorType);
BREAK_ON_FAIL(hr);
}
else
{
// add the stream to the sink writer
// with the exact same media type as the
// source stream
hr = m_pSinkWriter->AddStream(pStreamMediaType, &sinkStreamIndex);
BREAK_ON_FAIL(hr);
}
// make sure that the source stream index is equal to the sink stream index
if (sourceStreamIndex != sinkStreamIndex)
{
hr = E_UNEXPECTED;
break;
}
// increment the source stream index,
// so that on the next loop you are analyzing
// the next stream
sourceStreamIndex++;
// release the media type
pStreamMediaType = NULL;
}
BREAK_ON_FAIL(hr);
} while (false);
return hr;
}
GetTranscodeAudioType()
用于设置输出音频类型。我已经忘记了为什么目标格式是 MFAudioFormat_AAC
,欢迎您将其设置为 MFAudioFormat_MP3
。
//
// Get the target audio media type - use the AAC media format.
//
HRESULT GetTranscodeAudioType(
CComPtr<IMFMediaType>& pStreamMediaType)
{
HRESULT hr = S_OK;
do
{
BREAK_ON_NULL(pStreamMediaType, E_POINTER);
// wipe out existing data from the media type
hr = pStreamMediaType->DeleteAllItems();
BREAK_ON_FAIL(hr);
// reset the major type to audio since we just wiped everything out
pStreamMediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
BREAK_ON_FAIL(hr);
// set the audio subtype
hr = pStreamMediaType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_AAC);
BREAK_ON_FAIL(hr);
// set the number of audio bits per sample
hr = pStreamMediaType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16);
BREAK_ON_FAIL(hr);
// set the number of audio samples per second
hr = pStreamMediaType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 44100);
BREAK_ON_FAIL(hr);
// set the number of audio channels
hr = pStreamMediaType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2);
BREAK_ON_FAIL(hr);
// set the Bps of the audio stream
hr = pStreamMediaType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 16000);
BREAK_ON_FAIL(hr);
// set the block alignment of the samples
hr = pStreamMediaType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 1);
BREAK_ON_FAIL(hr);
} while (false);
return hr;
}
同样,GetTranscodeVideoType()
用于设置输出视频类型。
//
// Get the target video media type - use the H.264 media format.
//
HRESULT GetTranscodeVideoType(
CComPtr<IMFMediaType>& pStreamMediaType)
{
HRESULT hr = S_OK;
do
{
BREAK_ON_NULL(pStreamMediaType, E_POINTER);
// wipe out existing data from the media type
hr = pStreamMediaType->DeleteAllItems();
BREAK_ON_FAIL(hr);
// reset the major type to video since we just wiped everything out
pStreamMediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
BREAK_ON_FAIL(hr);
// set the video subtype
if (m_VideoCodec == VideoCodec::H264)
{
hr = pStreamMediaType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264);
}
else
{
hr = pStreamMediaType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_HEVC);
}
BREAK_ON_FAIL(hr);
// set the frame size to 720p as a 64-bit packed value
hr = MFSetAttributeSize(
pStreamMediaType, // attribute store on which to set the value
MF_MT_FRAME_SIZE, // value ID GUID
m_Width, m_Height); // frame width and height
BREAK_ON_FAIL(hr);
// Set the frame rate to 30/1.001 -
// the standard frame rate of NTSC television - as
// a 64-bit packed value consisting of a fraction of two integers
hr = MFSetAttributeRatio(
pStreamMediaType, // attribute store on which to set the value
MF_MT_FRAME_RATE, // value
m_VideoFPS, 1); // frame rate ratio
BREAK_ON_FAIL(hr);
// set the average bitrate of the video in bits per second -
// in this case 10 Mbps
hr = pStreamMediaType->SetUINT32(MF_MT_AVG_BITRATE, m_VideoBitrate);
BREAK_ON_FAIL(hr);
// set the interlace mode to progressive
hr = pStreamMediaType->SetUINT32(MF_MT_INTERLACE_MODE,
MFVideoInterlace_Progressive);
BREAK_ON_FAIL(hr);
// set the pixel aspect ratio to 1x1 - square pixels
hr = MFSetAttributeSize(pStreamMediaType, MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
BREAK_ON_FAIL(hr);
} while (false);
return hr;
}
GetTranscodeMediaType()
调用 GetTranscodeAudioType()
或 GetTranscodeVideoType()
来完成其工作。
//
// Set the target target audio and video media types to hard-coded values.
// In this case, you are setting audio to AAC, and video to 720p H.264
//
HRESULT GetTranscodeMediaType(
CComPtr<IMFMediaType>& pStreamMediaType)
{
HRESULT hr = S_OK;
GUID streamMajorType;
do
{
// extract the major type of the source stream from the media type
hr = pStreamMediaType->GetMajorType(&streamMajorType);
BREAK_ON_FAIL(hr);
// if this is an audio stream, configure a hard-coded AAC profile.
// If this is a video stream, configure an H.264 profile
if (streamMajorType == MFMediaType_Audio)
{
hr = GetTranscodeAudioType(pStreamMediaType);
}
else if (streamMajorType == MFMediaType_Video)
{
hr = GetTranscodeVideoType(pStreamMediaType);
}
} while (false);
return hr;
}
WriteVideoFrame()
将 m_pImage
复制到 pData
。并设置缓冲区长度。然后创建一个样本来附加 m_pBuffer
。当然,样本必须设置其采样时间和持续时间。最后,将样本发送到 SinkWriter
。
HRESULT WriteVideoFrame(
DWORD streamIndex,
const LONGLONG& rtStart
)
{
IMFSample *pSample = NULL;
BYTE *pData = NULL;
// Lock the buffer and copy the video frame to the buffer.
HRESULT hr = m_pBuffer->Lock(&pData, NULL, NULL);
if (SUCCEEDED(hr))
{
hr = MFCopyImage(
pData, // Destination buffer.
m_cbWidth, // Destination stride.
(BYTE*)m_pImage, // First row in source image.
m_cbWidth, // Source stride.
m_cbWidth, // Image width in bytes.
m_Height // Image height in pixels.
);
}
if (m_pBuffer)
{
m_pBuffer->Unlock();
}
// Set the data length of the buffer.
if (SUCCEEDED(hr))
{
hr = m_pBuffer->SetCurrentLength(m_cbBuffer);
}
// Create a media sample and add the buffer to the sample.
if (SUCCEEDED(hr))
{
hr = MFCreateSample(&pSample);
}
if (SUCCEEDED(hr))
{
hr = pSample->AddBuffer(m_pBuffer);
}
// Set the time stamp and the duration.
if (SUCCEEDED(hr))
{
hr = pSample->SetSampleTime(rtStart);
}
if (SUCCEEDED(hr))
{
hr = pSample->SetSampleDuration(m_FrameDuration);
}
// Send the sample to the Sink Writer.
if (SUCCEEDED(hr))
{
hr = m_pSinkWriter->WriteSample(streamIndex, pSample);
}
SafeRelease(&pSample);
return hr;
}
WriteAudioFrame()
从 m_pSourceReader
读取样本并将其发送到 SinkWriter
。
HRESULT WriteAudioFrame(DWORD streamIndex, LONGLONG& timestamp)
{
HRESULT hr = S_OK;
DWORD flags = 0;
CComPtr<IMFSample> pSample;
do
{
// pull a sample out of the source reader
hr = m_pSourceReader->ReadSample(
(DWORD)MF_SOURCE_READER_ANY_STREAM, // get a sample from any stream
0, // no source reader controller flags
&streamIndex, // get index of the stream
&flags, // get flags for this sample
×tamp, // get the timestamp for this sample
&pSample); // get the actual sample
BREAK_ON_FAIL(hr);
// The sample can be null if you've reached the end of stream or encountered a
// data gap (AKA a stream tick). If you got a sample, send it on. Otherwise,
// if you got a stream gap, send information about it to the sink.
if (pSample != NULL)
{
// push the sample to the sink writer
hr = m_pSinkWriter->WriteSample(streamIndex, pSample);
BREAK_ON_FAIL(hr);
}
else if (flags & MF_SOURCE_READERF_STREAMTICK)
{
// signal a stream tick
hr = m_pSinkWriter->SendStreamTick(streamIndex, timestamp);
BREAK_ON_FAIL(hr);
}
// if a stream reached the end, notify the sink,
// and increment the number of finished streams
if (flags & MF_SOURCE_READERF_ENDOFSTREAM)
{
hr = m_pSinkWriter->NotifyEndOfSegment(streamIndex);
BREAK_ON_FAIL(hr);
}
// release sample
pSample = NULL;
} while (false);
return hr;
}
Process()
是一个长时间运行的函数,它调用 InitializeWriter
然后进入一个无限循环。在循环开始时,它触发 m_evtRequest
请求帧,然后等待 OpenGL 线程回复 m_evtVideoEnded
或 m_evtReply
。如果信号是 m_evtReply
,则调用 WriteVideoFrame
和/或 WriteAudioFrame
将样本发送到 SinkWriter
。最后,调用 Finalize()
来完成视频文件。
const bool Process()
{
if (IsValid())
{
DWORD video_stream = 0;
DWORD audio_stream = 0;
HRESULT hr = InitializeWriter(&video_stream, &audio_stream);
if (SUCCEEDED(hr))
{
// Send frames to the sink writer.
LONGLONG rtStart = 0;
bool success = true;
HRESULT hr = S_OK;
bool audio_done = false;
DWORD audio_stream = 0;
LONGLONG audio_timestamp = 0;
while (true)
{
SetEvent(m_evtRequest);
success = true;
HANDLE arr[2];
arr[0] = m_evtVideoEnded;
arr[1] = m_evtReply;
DWORD dw = WaitForMultipleObjects(2, arr, FALSE, INFINITE);
if (WAIT_OBJECT_0 == dw)
{
OutputDebugStringA("VideoEnded");
break;
}
if (WAIT_OBJECT_0 + 1 != dw)
{
OutputDebugStringA("E_FAIL");
success = false;
break;
}
if (success)
{
hr = WriteVideoFrame(video_stream, rtStart);
if (FAILED(hr))
{
success = false;
break;
}
rtStart += m_FrameDuration;
if (m_MP3Filename.empty() == false)
{
if (rtStart < audio_timestamp)
continue;
if (!audio_done)
{
hr = WriteAudioFrame(audio_stream, audio_timestamp);
if (FAILED(hr))
{
audio_done = true;
}
}
}
}
}
if (success)
{
hr = m_pSinkWriter->Finalize();
}
m_pSinkWriter.Release();
return success;
}
}
return false;
}
在 Web 浏览器中以 asm.js 形式运行
本节主要关注演示中附带的 OpenGL 框架的 asm.js 部分。如果您只对视频编码器感兴趣,可以安全地忽略本节。如果您想在 Web 浏览器中使用该框架运行您的应用程序,那么本节适合您。框架的设计选择基于主流 WebGL 和 Web 浏览器环境能够工作的最低技术。由于 WebGL 是基于 OpenGL 2.0 ES 的安全子集,因此可以将 WebGL 调用轻松地转换为 OpenGL 2.0 调用。巧合的是,这也是该框架支持的最高 OpenGL 版本。
更改您的 URL
在 Program.cpp 的 SRC_FOLDER 中相应地更改您的 IP 地址/域名/端口。SRC_FOLDER 用于从那里下载您的资产。如果您不使用 https,请切换到 http
。
#ifdef __EMSCRIPTEN__
#define SRC_FOLDER "https://:44319/"
#else
#define SRC_FOLDER ".\\"
#endif
将代码重新编译为 asm.js
如果您在 Windows 10 上并且熟悉 Bash 命令,请重新编译代码以使新 URL 生效,启用 Windows Subsystem for Linux 并从 MS Store 安装 Ubuntu,然后安装 Emscripten。运行 GNU make
。
在 Makefile 中,更改为 -s WASM=0
make all
将代码重新编译为 Webassembly
删除或更改为 -s WASM=1
,因为当未指定 WASM 时,默认选项是编译为 Webassembly
。我之所以没有这样做,是因为 IIS 不识别添加到其中的 wasm
MIME 类型。
不支持 Assimp
Assimp 不被支持,仅仅因为它没有用于 Assimp 的Emscripten 端口。取而代之的是,使用 Tiny OBJ Loader 来加载简单的 Wavefront OBJ 格式的 3D 模型。OBJ 文件扩展名以 *.obj 和 *.mtl 结尾,我已经修改了库以加载 *.obj.txt 和 *.mtl.txt,因为我想避免在 Web 服务器中添加新的 MIME 类型。
下载您的资产
要在屏幕上绘制任何内容,请将您的类派生自 DrawableSceneComponent
,并且**必须**在构造函数中调用 DownloadFiles()
来从相对子文件夹下载您的资产,因为 asm.js 必须在主渲染循环运行之前下载所有资产。DownloadFiles
在桌面应用程序中调用 OpenFile
来直接处理文件。如果代码使用 EMSCRIPTEN
宏定义进行编译,它将首先下载文件然后打开。
不下载着色器
着色器代码是内联的 C++11 原始字符串字面量,以节省下载工作。
硬件加速
该库现在支持硬件加速。
v1.0.4 中构造函数的质量参数
int numWorkerThreads
:0 表示使用默认值int qualityVsSpeed
:[0:100] 0 为速度,100 为质量RateControlMode mode
:3 种模式可供选择UnconstrainedVBR
、Quality
、CBR
(VBR
是可变比特率,CBR
是恒定比特率)int quality
:仅当模式为 Quality 时有效。[0:100] 0 为较小的文件大小和较低的质量,100 为较大的文件大小和较高的质量
参考书籍
历史
- 2022 年 10 月 27 日:在引言中添加了 Mandy Frenzy 视频和链接
- 2022 年 3 月 25 日:添加了
HasH264()
和HasHEVC()
函数,用于检查系统上是否存在硬件加速或软件编码器。请记住在调用HasH264()
和HasHEVC()
之前调用CoInitialize()
。恢复了在虚拟机中进行测试的软件编码选项。
注意:选择软件编码时会忽略质量设置,因为它们不起作用。 - 2020 年 7 月 26 日:添加了 1.0.4 版本,用于调整质量和编码速度参数(有关更多信息,请参阅质量部分)
- 2020 年 7 月 25 日:添加了 1.0.3 版本,支持硬件加速(有关更多信息,请参阅硬件加速部分)
- 2019 年 10 月 25 日:更新了 OpenGL 框架以兼容最新的 Emscripten。修复了不正确的屏幕宽度和高度。
- 2018 年 12 月 18 日:首次发布