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

OpenGL 的 H264/HEVC 视频编码器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (18投票s)

2018年12月16日

CPOL

17分钟阅读

viewsIcon

46438

downloadIcon

1408

用于录制 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_ENCODEREMSCRIPTEN 宏进行保护。您可以自行使用自己的渲染器与视频编码器结合。默认的 OpenGL 渲染器仅用于演示。

文档分为三个主要部分。第一部分是关于如何设置演示并运行,以及如何修改参数。第二部分是关于如何将其集成到您的 OpenGL 框架中。该演示使用了 Paul Varcholik 的 OpenGL Essentials LiveLessons 中教授的渲染器框架。原始源代码使用了 GLFW 并基于 OpenGL 4.4:我将其框架转换为使用 SDL 并降级到 OpenGL 2.0。这样做的决定基于 WebGL 和 Emscripten 能支持的最低通用标准。稍后将有关于如何与 DirectX 集成的教程。理论上,此视频编码器应能很好地与其他图形 API(如 Vulkan)集成,毕竟它只需要一个视频缓冲区和一些同步即可完成工作。第三部分是关于视频编码器内部机制的解释,如果您不关心编码器内部实现,可以跳过。最后一部分解释了编译为 asm.jsWebassembly 所需的 Emscripten 部分。

运行演示

宇宙飞船视频

YouTube 上的宇宙飞船视频

spaceship

所有必需的库都包含在仓库中。所需的 DLL 文件在 Win32 的构建后会自动复制到 ReleaseDebug 文件夹。x64 构建由于无法在网上找到 x64 的 zlib 库/DLL 而无法构建;这是 OpenGL 渲染器的一个链接问题,而不是视频编码器的问题。

要查看 OpenGL 演示,请在 Visual Studio 中打开 SDL_App.sln 并构建 SDL_App 项目。

要运行视频编码演示,请打开 H264SinkWriter.cpp,并在 main 函数中,将 configFilemusicFilevideoFile 修改为您机器上的路径。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.txtImagesModels 文件夹复制到 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 视频

YouTube 上的 H264 视频

h264_small

Mandy Frenzy 的 HEVC 视频(有伪影)

YouTube 上的 HEVC 视频

hevc_small

更新:HEVC 质量问题已通过硬件加速解决。

正如您所见,HEVC 中存在正弦波伪影,而 H264 中不存在。增加比特率可以解决问题,但文件大小会增加。顺便说一句,正弦波是由三角形而不是线条渲染的,也不是由片段着色器渲染的。原因是线条在 OpenGL ES 2.0 中通常实现为 1 像素宽。使用三角形可以让我控制宽度/高度。

与您的 OpenGL 框架集成

本节介绍将视频编码器集成到您的渲染器中所需的修改。

查找所有编码相关代码的最快方法是在源代码中搜索 VIDEO_ENCODER 宏。编码器要求您实现两个全局函数:check_config_fileencoder_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)中传入屏幕 widthheightFPS 信息。实际上,您可以打开任何可以为您提供此信息的文档,因此 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() 用于设置每个帧的时间增量,例如,如果 FPS30,则时间增量应为 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 读取渲染后的缓冲区。代码等待 mEvtRequestmEvtExit。当编码线程请求新帧时,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 提供了三种编写视频编码器的方法

  1. Media Session
  2. Transcoder API
  3. 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_evtExitm_evtVideoEndedScene::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 来返回 widthheightfps

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();
}

GetSourceDurationH264Writer 中用于获取源 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_pSourceReaderpSource 用于获取和打印时长,这没有用。所以你可以删除 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 宏能够从中跳出。接下来,我们创建一个 SinkWriterMapStreams()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_FAILBREAK_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.txtMapStreams() 大部分也是我从别处复制的样板代码。

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
            &timestamp,                           // 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_evtVideoEndedm_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.cppSRC_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 种模式可供选择 UnconstrainedVBRQualityCBRVBR 是可变比特率,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 日:首次发布
© . All rights reserved.