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

从 HBitmap 创建电影

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (75投票s)

2003 年 9 月 25 日

18分钟阅读

viewsIcon

1595586

downloadIcon

26264

介绍以编程方式创建电影(AVI/WMV/MOV)的方法。

目录

引言

众所周知,电影是由一系列图像或位图组成的。同样,也知道 HBitmap 是位图的基本组成部分。在我们的所有 Windows 应用程序中,无论它们是动画还是静态界面,我们都有大量的 HBitmap。现在是时候将所有这些精美的 HBitmap 序列保存到一个文件中,并称之为电影、动画或演示,随你怎么叫。

以下介绍了一种从一系列 HBitmaps 创建电影(AVI / WMV / MOV)的方法。所需功能已封装在适当的类中,如 CAviFileCwmvFileCQTMovieFile。使用这些类非常简单,只需调用一个函数 AppendNewFrame(); 即可。所有必要的初始化(如帧率设置等)将在第一次调用 AppendNewFrame() 时由类本身处理。(QuickTime 类除外。它有自己的图形世界,需要通过调用 InitGraphicsWorld() 来显式初始化)。

正如人们所能轻易预料到的——这种方法是双管齐下的——对于那些想从一组图像文件(例如 *.jpg*.bmp)创建电影的人来说,只需要做的是——将所有这些图像加载到一个 HBitmap 数组中,然后按显示顺序用每个图像调用 AppendNewFrame()。对于那些想从程序生成的动画序列创建电影的人来说——只需将绘制的内容渲染到 HBitmap,并在每次更新时调用 AppendNewFrame()(可能在 WM_PAINTOnPaint() 处理程序中)。

在我们进一步讨论之前,我想提一下一点值得注意。我们将要讨论的这些类主要旨在为其他完整的应用程序提供附加支持(尽管它们也可以很好地用于您尚未设计的应用程序)。确切地说,这些类在设计时非常小心,以免干扰使用它们的应用程序的设计或功能。如果这些类中发生任何错误,它们宁愿关闭自己,而不是导致整个应用程序崩溃。因此,用户无需进行任何初始化和错误检查。如果一切顺利,您最终会得到一部精美的电影,但如果其中任何部分出错(在这些模块内部),您的应用程序仍然可以完美运行。

在接下来的章节中,将分别详细介绍每个类。

创建 AVI 电影

  • 包含文件:avifile.h
  • 实现文件:avifile.cpp
  • 附加库:vfw32.lib

CAviFile 类从 HBitmap 创建 AVI 电影。使用此类需要两个步骤。第一步是创建一个 CAviFile 对象。CAviFile 的构造函数已在 AviFile.h 中声明为

class CAviFile{
public:
    CAviFile(LPCSTR lpszFileName=_T("Output.avi"), 
             DWORD dwCodec = mmioFOURCC('M','P','G','4'),
             DWORD dwFrameRate = 1);
    ~CAviFile(void);
    HRESULT AppendNewFrame(HBITMAP hBitmap);
    HRESULT AppendNewFrame(int nWidth, 
                           int nHeight, 
                           LPVOID pBits, 
                           int nBitsPerPixel);
};

构造函数接受三个参数——输出文件名,默认为 Output.avi, 要使用的视频编解码器,默认为 MPG4,以及帧率(FPS),默认为 1。在创建 CAviFile 对象时,您可以选择使用默认参数值(在大多数情况下应该没问题),也可以提供自己的值。稍后将对此进行更详细的解释。

一旦使用适当的编解码器和帧率值创建了 CAviFile 对象,使用它的第二步就是实际调用 AppendNewFrame() 方法。从上面可以看出,该方法有两个重载版本。一种版本是

 HRESULT AppendNewFrame(HBITMAP hBitmap); 
当我们的所有绘制都已完成到 HBitmap 并准备好作为新帧添加到当前电影的末尾时,它很有用。另一种形式接受原始位图数据而不是 HBitmap,如下所示。
     HRESULT AppendNewFrame(int nWidth, 
                           int nHeight, 
                           LPVOID pBits, 
                           int nBitsPerPixel);
如果您渲染的绘图是以原始数据而不是 HBitmap 的形式存在的,您可能会更喜欢第二种版本。但是,应该注意的是,一旦我们开始使用一种形式,我们就不能在电影创建过程中在两者之间切换。

以下说明了使用 CAviFile 类所涉及的典型代码序列

#include "avifile.h" 
CAviFile aviFile;
OnPaint(){
    hdc = BeginPaint(hWnd, &ps);
    //...Drawing Code onto some hBitmap
    EndPaint(hWnd, &ps);
    aviFile.AppendNewFrame(hBackBitmap);
}

CAviFile::AppendNewFrame() 方法在成功时返回 S_OK,在失败时返回 E_FAIL。如果发生错误,我们可以使用 CAviFileGetLastErrorMessage() 方法以字符串格式检索错误消息描述。

LPCTSTR CAviFile::GetLastErrorMessage() const { return m_szErrMsg; }

典型用法如下所示

if(FAILED(avi.AppendNewFrame(hBackBitmap)))
{
    MessageBox(hWnd, avi.GetLastErrorMessage(), 
               _T("Error Occured"), MB_OK | MB_ICONERROR);
}

实现

本节简要介绍 CAviFile 类运行的幕后机制。您可以在实现文件 AviFile.cpp 本身中找到所有这些详细信息。

AVI 电影创建是最古老的电影创建形式之一,通过 AVIFile 函数集得到了大量支持。在调用任何 AVIFile 函数之前,我们应该调用 AVIFileInit(),在退出应用程序之前,我们应该调用 AVIFileExit()。构造函数和析构函数是这两者的最佳位置。因此,您可以在 CAviFile 类的构造函数和析构函数中找到它们。

在所有 AVIFile 函数中,我们感兴趣的是:AVIFileOpen()AVIFileRelease()AVIFileCreateStream()AVIMakeCompressedStream()AVIStreamSetFormat()AVIStreamWrite()

其中,AVIStreamWrite() 是将压缩图像数据实际写入电影文件的主要操作。所有其他函数用于设置文件、流和压缩选项。使用 AVIFileOpen() 创建/打开 AVI 文件后,可以通过为 AVISTREAMINFO 结构成员设置适当的值并通过函数 AVICreateStream() 来选择压缩选项和视频编解码器选项。例如,AVISTREAMINFOfccHandler 成员代表视频编解码器的四字符代码。通常,视频编解码器的四字符代码是一个字符串,如 "divx"、"mpg4" 等,每个系统上安装的视频编解码器都具有唯一性。函数 mmioFOURCC() 可用于将这些字符转换为 fccHandler 成员可接受的 DWORD 格式。默认情况下,在 CAviFile 实现中,我们使用 "mpg4" 编解码器。要为您的应用程序选择不同的编解码器,请在构造函数中按如下方式传递编解码器的 fourcc:

CAviFile  avi("Output.Avi", mmioFOURCC('D','I','V','X'), 1);
    // Use DivX codec with 1 FPS

四字符代码列表和其他相关信息可以在 这里 找到。

请注意,您可以将 fourcc 值设置为 0 以完全避免使用编解码器,这样您的位图将按原样插入电影,而不会被任何编解码器处理。

CAviFile  avi("Output.Avi", 0, 1); // Does not use any Codec !!

AVISTREAMINFOdwRate 成员控制电影的帧率。5 到 15 之间的值是常见的,对于大多数动画来说应该是不错的(dwRate = 15 通常表示每秒 15 帧)。您可以通过在 CAviFile 构造函数的第三个参数(dwFrameRate)中传递您选择的值来更改此帧率设置。

一旦所有这些设置都成功完成,我们就可以使用 AVICreateStream() 函数在电影文件中创建一个新的视频流,之后我们可以调用 AVIMakeCompressedStream() 方法来设置创建流的压缩过滤器。AVIMakeCompressedStream() 的成功取决于您使用的编解码器在系统中是否可用。如果您使用了无效的 fourcc 值或机器上没有该编解码器,则调用 AVIMakeCompressedStream() 将会失败。

最后,在设置了压缩设置但开始写入实际图像序列之前,我们需要设置视频流的格式,这是通过使用 AVIStreamSetFormat 完成的。AVIStreamSetFormat() 的成功取决于输入位图数据是否符合压缩器(编解码器)的要求。请注意,每个编解码器对处理其输入数据的要求都不同(例如,每像素位数是 4 的倍数,或者帧的宽度和高度是 2 的幂次方等),并且传递不符合这些要求的位图(或数据)可能会导致 AviStreamSetFormat() 失败。

一旦设置了流格式,我们就可以使用 AVIStreamWrite() 函数开始写入 HBitmap 数据。此函数会自动压缩数据(使用我们之前设置的选项)并将其写入将保存到输出电影文件中的视频流。完成后,应使用 AVIFileRelease() 函数关闭电影文件以刷新所有缓冲区。

上述 AVIFile 函数集声明在标准头文件 vfw.h 中,并且应链接的相应库是 vfw32.lib

创建 WMV 电影

  • 包含文件:wmvfile.h
  • 实现文件:wmvfile.cpp
  • 附加库:wmvcore.lib

CwmvFile 类从 HBitmap 创建 WMV 电影。此类基于 Windows Media Format SDK。库文件 wmvcore.lib 是 SDK 的一部分,可以从 Microsoft 下载。

默认情况下,CwmvFile 的实现使用 Windows Media Format SDK 版本 9.0。它在文件 wmvfile.h 中定义为

#define WMFORMAT_SDK_VERSION WMT_VER_9_0

但是,您可以通过使用适当的版本号将其更改为其他版本。在您的主应用程序中添加以下行,可以使实现使用 Media Format SDK 版本 8.0 而不是默认的 9.0 版本

#define WMFORMAT_SDK_VERSION WMT_VER_8_0

在包含 wmvfile.h 之前使用上述 #define,以便可以忽略 wmvfile.h 中的默认 #define。

使用 CwmvFile 类需要两个步骤。第一步是创建一个 CwmvFile 对象。CwmvFile 的构造函数已在 wmvFile.h 中声明为

class CwmvFile{
public:
    CwmvFile(LPCTSTR lpszFileName = _T("Output.wmv"),
            const GUID& guidProfileID = WMProfile_V80_384Video,
            DWORD dwFrameRate = 1);
    ~CwmvFile(void);
    HRESULT AppendNewFrame(HBITMAP hBitmap);
    HRESULT AppendNewFrame(int nWidth, 
                           int nHeight, 
                           LPVOID pBits, 
                           int nBitsPerPixel);
};

构造函数接受三个参数——输出文件名,默认为 Output.wmv,然后是配置文件 ID,最后是帧率选项。配置文件是一组用于创建 WMV 电影文件的媒体参数。配置文件 ID 是分配给配置文件的唯一 GUID。大多数常用系统配置文件的 ID 列在 MSDN 上。

并非所有这些配置文件都可能在您的系统上可用。使用系统中不存在的配置文件 ID 将导致 CwmvFile 对象创建失败。您可以使用源文件 EnumProfiles.cpp(在 EnumProfiles.zip 中)提供的 EnumProfiles() 函数来枚举系统中所有可用的配置文件。EnumProfiles() 函数会输出您机器上所有可用配置文件的名称。您可以在上述系统配置文件网页上查找配置文件名称对应的配置文件 ID。

第二步使用 CwmvFile 类涉及实际调用 AppendNewFrame()。从上面可以看出,它有两个重载版本。一种是 HBitmap 样式——其中所有绘制都已完成到 HBitmap,并且准备好添加到当前电影的末尾。另一种形式接受原始位图数据而不是 HBitmap。如果我们渲染的绘图是以数据而不是 HBitmap 的形式存在的——我们也可以使用此选项。但是,应该注意的是,一旦我们开始使用其中一种,我们就不能在电影创建过程中在两者之间切换。以下说明了代码序列

#define WMFORMAT_SDK_VERSION WMT_VER_8_0
#include "wmvfile.h"
CwmvFile wmvFile("wmvFile.wmv", WMProfile_V80_384PALVideo, 1);
OnPaint()
{
    hdc = BeginPaint(hWnd, &ps);
    //...Drawing Code onto some hBitmap
    EndPaint(hWnd, &ps);
    wmvFile.AppendNewFrame(hBackBitmap);
}

值得注意的是,某些编解码器可能要求帧的高度和宽度是某个整数(如 3 或 4 等)的倍数。也就是说,您不能拥有任意尺寸的视频高度或宽度。它们有限制,例如某些编解码器只能处理 320*240 或 640*480。因此,您应该使用支持的尺寸创建 HBitmap,或者将其绘制到一个具有支持尺寸的新位图中,然后使用该位图。使用不支持尺寸的 HBitmap 会导致错误。

实现

本节简要介绍 CwmvFile 类运行的幕后机制。您可以在实现文件 wmvfile.cpp 本身中找到所有这些详细信息。

CwmvFile 类维护三个私有函数,名为 AppendFrameFirstTime()AppendFrameUsual()AppendDummy()。第一个函数 AppendFrameFirstTime() 包含所有初始化代码,用于设置电影属性,如帧的宽度和高度等,并且只调用一次——只为第一帧调用。对于其余帧,AppendFrameUsual() 负责处理。它包含将数据样本写入电影文件的实际代码,因此为每一帧调用。AppendDummy() 只是一个占位符函数,顾名思义,它什么也不做。此外,还定义了一个函数指针 pAppendFrame。当应用程序启动时,在没有向电影文件添加任何帧的情况下——函数指针指向 AppendFrameFirstTime() 函数。一旦添加了第一帧并正确初始化了所有电影文件属性,指针就会更新以指向 AppendFrameUsual() 函数。现在,当应用程序在 CwmvFile 对象上调用 AppendNewFrame() 时——该函数所做的只是调用 pAppendFrame 指针指向的函数。因此,第一次将是 AppendFrameFirstTime() 函数,从那时起将是 AppendFrameUsual() 函数。

但是,当发生错误时,指针 pAppendFrame 将被指向 AppendDummy(),以便从那时起不再向电影添加帧,只执行虚拟代码并返回失败值。这使得您即使在电影创建部分发生错误时也能继续动画。但是,您可以检查代码中的返回值以确定成功状态,如果您愿意,可以在错误时终止应用程序。

创建电影的实际过程如下。Windows Media Format SDK 支持 IWMProfileIWMWriterIWMInputMediaPropsIWMProfileManager 等 COM 接口来处理电影操作。IWMWriter 是实际支持将图像流写入电影文件的接口。要为此接口创建一个对象,我们需要使用 WMCreateWriter() 函数。这将为我们提供 IWMWriter 接口的指针。但是,在我们实际使用 IWMWriter 对象进行写入之前,我们需要为写入器设置配置文件。为此,我们需要加载由配置文件 ID 指定的配置文件。我们可以通过使用 IWMProfileManager 对象来做到这一点。我们使用 WMCreateProfileManager() 函数创建一个 IWMProfileMnager 对象,然后在创建的配置文件管理器对象上调用 IWMProfileManager::LoadProfileById() 方法。这将为我们提供加载的配置文件接口的指针,我们可以通过使用 IWMWriter::SetProfile() 与写入器对象一起使用。

可以使用 IWMWriter::SetOutputFileName() 方法设置电影输出文件名。实际的输入视频属性通过使用 IWMWriter::SetInputProps() 方法设置。IWMInputMediaProps 接口支持设置各种输入属性,如帧率、源帧矩形大小等(请注意,如前所述,这些帧大小应符合视频编解码器和配置文件的要求)。在我们实际开始将图像数据写入电影文件之前,我们应该调用 IWMWriter::BeginWriting() 方法。IWMWriter::WriteSample() 方法实际上将图像数据流写入电影文件。但是,它期望数据以 INSSBuffer 的形式存在。因此,对于每一帧,我们需要使用 IWMWriter::AllocateSample() 方法创建一个 INSSBuffer 对象,并使用返回的缓冲区来保存图像数据并将其传递给 IWMWriter::WriteSample()。我们应该使用 INSSBuffer::Release() 方法释放缓冲区。请注意,缓冲区应该为每个输入帧重新创建。一旦我们完成了图像流的写入,我们应该调用 IWMWriter::EndWriter() 方法来完成写入。最后,在终止应用程序之前,我们应该使用每个对象的 Release() 方法来释放我们所有的对象。

请注意,在我们实际执行任何 COM 对象操作之前,我们应该使用 CoInitialize() 函数启用 COM,并在释放所有 COM 对象后,使用 CoUninitialize() 函数关闭 COM。这些函数分别放置在 CwmvFile 的构造函数和析构函数中。

创建 QuickTime 电影

  • 包含文件:QTMoviefile.h
  • 实现文件:QTMoviefile.cpp
  • 附加库:QtmlClient.lib

我们的 CQTMovieFile 类从 HBitmap 创建 QuickTime 电影。此类基于 QuickTime 6 SDK for Windows。库文件 QtmlClient.lib 是 SDK 的一部分,可以从 这里 下载。

使用 CQTMovieFile 需要三个步骤。第一步是创建 CQTMovieFile 对象。该类的构造函数声明为

class CQTMovieFile
{
public:
    CQTMovieFile();
    HRESULT InitGraphicsWorld(HDC hBackDC,HBITMAP hBackBitmap,
        LPCTSTR lpszFileName=_T("Output.mov"));
    ~CQTMovieFile(void);
    HRESULT AppendNewFrame();
};

QuickTime 使用自己的图形世界进行操作,就像 Windows 使用 GDI 一样。因此,需要建立我们的 GDI 对象 HBitmap 和 QuickTime 内部图形世界之间的连接。这是通过 InitGraphicsWorld() 函数完成的。涉及使用 HBitmap 创建 QuickTime 电影的方法如下:QuickTime 电影只不过是其图形世界的一系列帧(就像 AVI 文件是一系列位图一样)。因此,如果我们能将动画绘制到 QuickTime 的图形世界,那么所有这些都可以轻松地转换为 QuickTime 电影。这是由我们的 InitGrpahicsWorld() 函数实现的。它所做的只是——告诉图形世界不要使用自己的内存,而是使用我们提供给它的 HBitmap 的内存。

一旦我们成功完成了 InitGraphicsWorld() 的调用,QuickTime 的图形世界和 Windows 的 GDI 之间就会建立起一种关系。我们提供的 HBitmap 作为这两个世界的连接。我们在 HBitmap 上绘制的任何内容都会影响 QuickTime 的图形世界(因为它是使用 HBitmap 的数据作为其图形世界内存),而 QuickTime 对其图形世界所做的任何操作都会反映到我们的 HBitmap 上。

使用这种新建立的 QuickTime 和 GDI 之间的连接来创建 QuickTime 电影非常简单。将动画的每一帧渲染到 HBitmap,然后调用 AppendNewFrame() 函数将此帧添加到电影文件中。请注意,AppendNewFrame() 没有参数。这是因为 QuickTime 已经拥有了它所需的所有数据,并且以图形世界的形式随时可用。另外请注意,与 CAviFileCwmvFile 不同,CQTMovieFile 没有 AppendNewFrame() 函数的重载原始数据版本。因此,您只能将 HBitmapCQTMovieFile 类一起使用。以下说明了代码序列

#include "QTMovieFile.h"
CQTMovieFile movFile;
OnCreate(){
    InitGraphicsWorld(hBackDC,hBackBitmap);
}
OnPaint(){
    hdc = BeginPaint(hWnd, &ps);
    //...Drawing Code onto the hBackBitmap
    EndPaint(hWnd, &ps);
    movFile.AppendNewFrame();
}

用作 QuickTime 图形世界和 Windows GDI 之间连接的 HBitmap 应该使用 CreateDIBSection() 函数创建。另外请注意,要与图形世界一起使用的 HBitmap 应该是自顶向下位图,因此在调用 CreateDIBSecion 时,BITMAPINFO 结构的 BITMAPINFOHEADER 的高度值应设置为负数。在创建位图时使用正值高度将生成自底向上位图,因此会导致生成颠倒的 QuickTime 电影。

实现

本节简要介绍 CQTMovieFile 类运行的幕后机制。您可以在实现文件 QTMoviefile.cpp 本身中找到所有这些详细信息。

根据 QuickTime SDK——在访问任何 QuickTime 函数之前,应调用 InitializeQTML() 函数,并在关闭应用程序之前调用 TerminateQTML()。因此,CQTMovileFile 类分别在构造函数和析构函数中调用它们。另一个问题是——在调用任何电影操作之前,我们应该调用 EnterMovies(),并在终止时调用 ExitMovies()。这些也分别放置在构造函数和析构函数中。

QuickTime 提供了 CreateMovieFile()NewMovieTrack()NewTrackMedia()AddMediaSample()InsertMediaIntoTrack()AddMovieResource() 等函数来操作和控制电影数据和文件的行为,以及 CompressImage() 等压缩函数,使电影创建变得容易。有关这些的详细文档可以在 这里 找到。

对于 QuickTime 电影,视频编解码器可以从多种类型中选择。默认情况下,实现使用 kJPEGCodecType。您可以通过在应用程序中 #defining VIDEO_CODEC_TYPE 来覆盖此设置,如下所示:

#define VIDEO_CODEC_TYPE kAnimationCodecType 

这应该在 #including QTMovieFile.h 之前 #defined

有关所有可用编解码器的列表,请访问 此链接

结论

上面讨论的所有方法都旨在实现一个目标——从 HBitmap 创建电影。但是,结果可能因各种设置而异,从视频帧率设置到使用的编解码器。例如,对于某些屏幕捕获应用程序,选择一种称为 Windows Media Video 9 Screen codec 的特定编解码器在质量和大小上都应获得最佳结果——尽管这对于高比特率动画应用程序可能不是这样(有关以编程方式捕获屏幕的更多详细信息,请参阅文章 Capturing the Screen)。此外,为您的内容选择的特定格式(AVI /WMV /MOV)取决于许多因素,例如每种格式产生的电影的大小、产生的电影的质量以及跨平台兼容性等问题。虽然 QuickTime 提供了广泛的平台覆盖,但 Windows Media 格式正在迅速取代 AVI 格式,并且有望提供更好的质量。无论如何,下次您看到 HBitmap 时——不要忘记检查您是否可以从中制作一部不错的电影。

© . All rights reserved.