录屏程序之屏幕实时录制保存成AVI视频文件

Viewer

发布日期: 2018-12-15 17:04:24 浏览量: 2270
评分:
star star star star star star star star star_border star_border
*转载请注明来自write-bug.com

背景

之前自己在视频教程的时候,就从网上找了个破解版的录屏软件来录制视频。虽然是破解版的,但是破解不完全,因为录制的视频播放到十几分钟之后,就开始显示未注册版本的字样在屏幕中,所以很是碍眼啊。

后来,自己细想之后,觉得自己完全可以写一个专属的录屏软件,功能不用太复杂,就支持画面录制和声音录制,存储为视频文件就好。是的,后来我开发出来了。所以,才有了这篇分享文章给大家,来总结下开发心得,同时也向大家剖析下这个程序的实现过程和原理。

这篇文档分成上、下两个部分,上部分主要讲解实现实时录制屏幕画面,保存为视频文件。下部分主要讲解实现实时录制声音,并将声音添加到视频文件中。现在,本文是上部分,讲解实时录制画面的过程。

函数介绍

AVIFileInit 函数

初始化AVIFile库。
AVIFile库维护初始化次数的计数,而不是释放次数。 使用AVIFileExit函数释放AVIFile库并减少引用计数。 在使用任何其他AVIFile功能之前调用AVIFileInit。
此功能取代过时的AVIStreamInit函数。

函数声明

  1. STDAPI_(VOID) AVIFileInit(void);

参数

  • 无参数。

返回值

  • 无返回值。

AVIFileOpen 函数

打开AVI文件,并返回用于访问它的文件接口的地址。 AVIFile库维护文件打开次数的计数,而不是释放文件的次数。 使用AVIFileRelease函数释放文件并减少计数。

  1. STDAPI AVIFileOpen(
  2. PAVIFILE *ppfile,
  3. LPCTSTR szFile,
  4. UINT mode,
  5. CLSID pclsidHandler
  6. );

参数

  • ppfile
    指向接收新的IAVIFile接口指针的缓冲区。

  • szFile
    以Null结尾的字符串,其中包含要打开的文件的名称。

  • mode
    打开文件时使用的访问模式。 默认访问模式为OF_READ。 可以使用AVIFileOpen指定以下访问模式:

VALUE MEANING
OF_CREATE 创建一个新文件。 如果文件已经存在,它将被截断为零长度
OF_PARSE 跳过耗时的操作,如建立索引。 如果要使函数尽可能快地返回,请设置此标志,例如,如果要查询文件属性但未读取该文件
OF_READ 打开文件读取数据
OF_READWRITE 打开文件读取和写入数据
OF_SHARE_DENY_NONE 打开文件非排他性。 其他进程可以通过读或写访问来打开文件。 如果另一个进程以兼容模式打开文件,AVIFileOpen将失败
OF_SHARE_DENY_READ 打开文件非排他性。 其他进程可以用写访问来打开文件。 如果另一个进程以兼容模式打开文件或对其进行了读取访问,则AVIFileOpen将失败
OF_SHARE_DENY_WRITE 打开文件非排他性。 其他进程可以打开具有读访问权限的文件。 如果另一个进程以兼容性模式打开文件或对其进行写入访问,AVIFileOpen将失败
OF_SHARE_EXCLUSIVE 打开文件并拒绝其他进程的访问。 如果任何其他进程已打开文件,AVIFileOpen将失败
OF_WRITE 打开文件写入数据
  • pclsidHandler
    指向要使用的标准或自定义处理程序的类标识符。 如果值为NULL,系统将根据文件扩展名或文件中指定的RIFF类型从注册表中选择一个处理程序。

返回值

  • 如果成功返回零,否则返回错误。

AVIFileCreateStream 函数

在现有文件中创建一个新流,并创建一个新流的接口。

函数声明

  1. STDAPI AVIFileCreateStream(
  2. PAVIFILE pfile,
  3. PAVISTREAM *ppavi,
  4. AVISTREAMINFO *psi
  5. );

参数

  • pfile
    处理一个打开的AVI文件。
  • ppavi
    指向新流接口的指针。
  • psi
    指向包含有关新流的信息的结构,包括流类型和采样率。

返回值

  • 如果成功返回零,否则返回错误。 除非文件已经被写入权限打开,否则此函数返回AVIERR_READONLY。

AVIMakeCompressedStream 函数

从未压缩的流和压缩过滤器创建压缩流,并将指针的地址返回到压缩流。 此功能支持音频和视频压缩。

函数声明

  1. STDAPI AVIMakeCompressedStream(
  2. PAVISTREAM *ppsCompressed,
  3. PAVISTREAM psSource,
  4. AVICOMPRESSOPTIONS *lpOptions,
  5. CLSID *pclsidHandler
  6. );

参数

  • ppsCompressed
    指向接收压缩流指针的缓冲区。
  • psSource
    指向要压缩的流的指针。
  • lpOptions
    指向要使用的压缩类型的结构的指针以及要应用的选项。 您可以通过在AVICOMPRESSOPTIONS结构中标识适当的处理程序来指定视频压缩。 对于音频压缩,请指定压缩数据格式。
  • pclsidHandler
    指向用于创建流的类标识符。

返回值

  • 如果成功则返回AVIERR_OK,否则返回错误。

AVIStreamSetFormat 函数

设置指定位置的流的格式。

函数声明

  1. STDAPI AVIStreamSetFormat(
  2. PAVISTREAM pavi,
  3. LONG lPos,
  4. LPVOID lpFormat,
  5. LONG cbFormat
  6. );

参数

  • pavi
    处理开放的流。
  • lPos
    在流中接收格式的位置。
  • lpFormat
    指向包含新格式的结构的指针。
  • cbFormat
    lpFormat引用的内存块的大小(以字节为单位)。

返回值

  • 如果成功返回 0,否则返回错误。

AVIStreamWrite 函数

将数据写入流。

函数声明

  1. STDAPI AVIStreamWrite(
  2. PAVISTREAM pavi,
  3. LONG lStart,
  4. LONG lSamples,
  5. LPVOID lpBuffer,
  6. LONG cbBuffer,
  7. DWORD dwFlags,
  8. LONG *plSampWritten,
  9. LONG *plBytesWritten
  10. );

参数

  • pavi
    处理开放的流。
  • lStart
    第一个样本要写。
  • lSamples
    要写的样本数。
  • lpBuffer
    指向包含要写入数据的缓冲区。
  • cbBuffer
    lpBuffer引用的缓冲区大小。
  • dwFlags
    与此数据相关的标记。AVIIF_KEYFRAME,表示此数据不依赖于文件中的前面的数据。
  • plSampWritten
    指向缓冲区的指针,该缓冲区接收写入的样本数。 这可以设置为NULL。
  • plBytesWritten
    指向缓冲区的指针,用于接收写入的字节数。 这可以设置为NULL。

返回值

  • 如果成功返回 0,否则返回错误。

实现原理

这个录屏程序的实时屏幕画面录制部分,可以大致分成 4 个部分,分别是:视频文件 AVI 文件的创建级初始化、屏幕画面的截屏并获取截屏数据、对视频流添加画面帧、结束录制。现在,我们就分别针对每一部分的实现原理进行解析:

创建 AVI 视频文件及初始化

  • 首先,调用 AVIFileInit 函数初始化 AVIFile 库,因为下面要调用该库中的函数

  • 然后,调用 AVIFileOpen 函数根据文件路径以及创建标志,创建一个可写入的 AVI 视频文件

  • 接着,对 HAVI 结构体中的数据进行初始化,设置文件接口以及帧距

  • 最后,关闭 AVIFile 库

获取屏幕截屏数据

  • 首先,我们先使用 GetDesktopWindow 获取桌面窗口的句柄,再使用 GetDC 函数获取桌面窗口的设备上下文

  • 然后,创建一个与桌面窗口设备上下文兼容的内存设备上下文,并获取计算机屏幕的宽和高,以此来创建一个兼容的位图,并选中到兼容设备上下文中

  • 接着,我们就可以把桌面的画面内容复制到兼容的设备上下文档中,保存在兼容的内存位图里。再继续把光标绘制到画面上。这样,就可以成功获取屏幕和鼠标的画面了

  • 然后,调用 GetDIBits 函数,根据位图句柄获取位图中的数据

添加画面帧

在获取截图画面数据后,再初始化位图信息头结构 BITMAPINFOHEADER,将数据和信息头传递给添加画面帧函数,将画面添加到视频流当中。那么,为视频添加画面帧的流程如下:

  • 首先,调用 AVIFileInit 函数初始化 AVIFile 库,因为下面要调用该库中的函数

  • 然后,先判断视频流接口有没有创建,若没有创建,则接下来就开始创建视频流接口。

    • 首先设置视频流信息 AVISTREAMINFO,设置流类型为视频流、帧距、样本帧率、缓存大小、视频尺寸等

    • 然后,调用 AVIFileCreateStream 按照设置的流信息创建视频流,获取视频流接口

  • 接着,判断视频压缩流接口有没有创建,若没有创建,则接下来就开始创建压缩视频流接口

    • 首先,设置视频流压缩信息 AVICOMPRESSOPTIONS,指定使用的压缩编码器 xvid

    • 然后,调用 AVIMakeCompressedStream 按照压缩流格式创建视频压缩流,并获取压缩视频流接口

    • 若能成功获取视频压缩流接口,便调用 AVIStreamSetFormat 函数,设置压缩视频流的格式;若不能获取压缩视频流,则对原始的视频流,设置视频流格式

  • 若有压缩视频流,则调用 AVIStreamWrite 函数,将位图画面数据写入到压缩视频流中。若没有压缩视频流,则写入到原始视频流中。写入帧数是 1 帧,并更新 HAVI 结构中的记录下一帧的成员

  • 最后,关闭 AVIFile 库

结束录制

  • 首先,调用 AVIFileInit 函数初始化 AVIFile 库,因为下面要调用该库中的函数

  • 然后,根据 HAVI 机构中的数据,判断视频流接口、压缩视频流接口、音频流接口、文件接口等是否存在,若存在,则调用 AVIStreamRelease 进行释放

  • 最后,关闭 AVIFile 库

编码实现

初始化工作及创建 AVI 视频文件

  1. // *************** 创建avi文件 ****************
  2. // pszFileName avi文件完整路径
  3. // iFramePeriod 相邻帧时间间隔
  4. // pwaveFormatEx 如果不添加音频,pwaveFormatEx为NULL
  5. PHAVI CreateAvi(char* pszFileName, DWORD dwFramePeriod, WAVEFORMATEX *pwaveFormatEx)
  6. {
  7. // AVI文件环境初始化
  8. ::AVIFileInit();
  9. PHAVI pAvi = new HAVI;
  10. ::RtlZeroMemory(pAvi, sizeof(HAVI));
  11. // 创建AVI
  12. IAVIFile *pFile = NULL;
  13. HRESULT hr = ::AVIFileOpen(&pFile, pszFileName, OF_WRITE | OF_CREATE, NULL);
  14. if (AVIERR_OK != hr)
  15. {
  16. ShowError("AVIFileOpen");
  17. return pAvi;
  18. }
  19. // 初始化 HAVI 结构体
  20. // avi接口
  21. pAvi->pAviFile = pFile;
  22. if (NULL != pwaveFormatEx)
  23. {
  24. // 音频流创建时使用
  25. ::RtlCopyMemory(&pAvi->waveFormatEx, pwaveFormatEx, sizeof(WAVEFORMATEX));
  26. }
  27. // 视频流创建时使用
  28. pAvi->dwPeriod = dwFramePeriod;
  29. // 退出AVI文件
  30. ::AVIFileExit();
  31. return pAvi;
  32. }

向视频文件添加视频帧

  1. // *************** 添加视频帧 ****************
  2. // hAvi句柄
  3. // bmpInfoHeader 位图信息头
  4. // lpBmpData 指向位图数据的指针,必须指向 DIBSection.
  5. HRESULT AddAviFrame(PHAVI pAvi, BITMAPINFOHEADER bmpInfoHeader, PVOID lpBmpData)
  6. {
  7. // AVI文件环境初始化
  8. ::AVIFileInit();
  9. if (NULL == lpBmpData)
  10. {
  11. return AVIERR_BADPARAM;
  12. }
  13. // 若不存在原始视频流,则创建
  14. if (NULL == pAvi->pStream)
  15. {
  16. // 设置视频流信息
  17. AVISTREAMINFO aviStreamInfo = { 0 };
  18. ::RtlZeroMemory(&aviStreamInfo, sizeof(AVISTREAMINFO));
  19. // 流类型
  20. aviStreamInfo.fccType = streamtypeVIDEO;
  21. // 编码器
  22. aviStreamInfo.fccHandler = NULL;
  23. // 帧距
  24. aviStreamInfo.dwScale = pAvi->dwPeriod;
  25. // 样本帧数
  26. aviStreamInfo.dwRate = 1000;
  27. // 缓存大小
  28. aviStreamInfo.dwSuggestedBufferSize = bmpInfoHeader.biSizeImage;
  29. // 视频尺寸
  30. ::SetRect(&aviStreamInfo.rcFrame, 0, 0, bmpInfoHeader.biWidth, bmpInfoHeader.biHeight);
  31. // 创建视频流
  32. HRESULT hr = ::AVIFileCreateStream(pAvi->pAviFile, &pAvi->pStream, &aviStreamInfo);
  33. if (AVIERR_OK != hr)
  34. {
  35. ShowError("AVIFileCreateStream");
  36. return hr;
  37. }
  38. /* // 如果压缩视频流成功, 那么这里不能设置格式
  39. // 设置视频流格式
  40. hr = ::AVIStreamSetFormat(pAvi->pStream, 0, &bmpInfoHeader, sizeof(bmpInfoHeader));
  41. if (AVIERR_OK != hr)
  42. {
  43. ShowError("AVIStreamSetFormat");
  44. return hr;
  45. }
  46. */
  47. }
  48. // 若不存在压缩视频流,则创建
  49. if (NULL == pAvi->pStreamCompressed)
  50. {
  51. // 设置压缩选项
  52. AVICOMPRESSOPTIONS aviCompressOptions = { 0 };
  53. ::RtlZeroMemory(&aviCompressOptions, sizeof(AVICOMPRESSOPTIONS));
  54. aviCompressOptions.fccHandler = mmioFOURCC('x', 'v', 'i', 'd');
  55. // 创建压缩视频流
  56. HRESULT hr = ::AVIMakeCompressedStream(&pAvi->pStreamCompressed, pAvi->pStream, &aviCompressOptions, NULL);
  57. if (AVIERR_OK == hr)
  58. {
  59. // 设置压缩视频流格式
  60. ::AVIStreamSetFormat(pAvi->pStreamCompressed, 0, &bmpInfoHeader, sizeof(BITMAPINFOHEADER));
  61. }
  62. else
  63. {
  64. // 设置视频流格式
  65. ::AVIStreamSetFormat(pAvi->pStream, 0, &bmpInfoHeader, sizeof(BITMAPINFOHEADER));
  66. }
  67. }
  68. // 当没有压缩视频流时, 使用视频流
  69. IAVIStream *pStream = NULL;
  70. if (pAvi->pStreamCompressed)
  71. {
  72. pStream = pAvi->pStreamCompressed;
  73. }
  74. else if (pAvi->pStream)
  75. {
  76. pStream = pAvi->pStream;
  77. }
  78. else
  79. {
  80. return S_OK;
  81. }
  82. // 添加帧到视频流
  83. HRESULT hr = ::AVIStreamWrite(pStream,
  84. pAvi->dwNextFrame,
  85. 1,
  86. lpBmpData,
  87. bmpInfoHeader.biSizeImage,
  88. AVIIF_KEYFRAME,
  89. NULL,
  90. NULL);
  91. if (AVIERR_OK != hr)
  92. {
  93. ShowError("AVIStreamWrite");
  94. return hr;
  95. }
  96. pAvi->dwNextFrame++;
  97. // 退出AVI文件
  98. ::AVIFileExit();
  99. return S_OK;
  100. }

录制结束

  1. // *************** 关闭avi文件 ****************
  2. // hAvi 要关闭的avi句柄
  3. HRESULT CloseAvi(PHAVI pAvi)
  4. {
  5. // AVI文件环境初始化
  6. ::AVIFileInit();
  7. if (NULL == pAvi)
  8. {
  9. return S_OK;
  10. }
  11. // 删除接口引用并关闭avi
  12. if (NULL != pAvi->pAviFile)
  13. {
  14. ::AVIFileRelease(pAvi->pAviFile);
  15. pAvi->pAviFile = NULL;
  16. }
  17. // 释放音频流
  18. if (NULL != pAvi->aStream)
  19. {
  20. ::AVIStreamRelease(pAvi->aStream);
  21. pAvi->aStream = NULL;
  22. }
  23. // 释放压缩视频流
  24. if (NULL != pAvi->pStreamCompressed)
  25. {
  26. ::AVIStreamRelease(pAvi->pStreamCompressed);
  27. pAvi->pStreamCompressed = NULL;
  28. }
  29. // 释放原始视频流
  30. if (NULL != pAvi->pStream)
  31. {
  32. ::AVIStreamRelease(pAvi->pStream);
  33. pAvi->pStream = NULL;
  34. }
  35. // 释放内存
  36. if (pAvi)
  37. {
  38. delete pAvi;
  39. pAvi = NULL;
  40. }
  41. // 退出AVI文件
  42. ::AVIFileExit();
  43. return S_OK;
  44. }

屏幕截屏

  1. void Recording(PHAVI pAvi)
  2. {
  3. int iRet = 0;
  4. HRESULT hr = S_OK;
  5. // 获取桌面屏幕上下文,并创建一个兼容内存上下文
  6. HDC hDCSource = ::GetDC(::GetDesktopWindow());
  7. HDC hDCMem = ::CreateCompatibleDC(hDCSource);
  8. // 获取屏幕分辨率
  9. int iScreenWidth = ::GetSystemMetrics(SM_CXSCREEN);
  10. int iScreenHeight = ::GetSystemMetrics(SM_CYSCREEN);
  11. // 创建位图共内存上下文使用
  12. HBITMAP hDesktopBmp = ::CreateCompatibleBitmap(hDCSource, iScreenWidth, iScreenHeight);
  13. HBITMAP oldBmp = (HBITMAP)::SelectObject(hDCMem, hDesktopBmp);
  14. // 将桌面屏幕上下文绘制到兼容内存上下文
  15. ::BitBlt(hDCMem, 0, 0, iScreenWidth, iScreenHeight, hDCSource, 0, 0, SRCCOPY);
  16. // 加载光标
  17. HCURSOR hCursor = ::LoadCursor(NULL, IDC_ARROW);
  18. // 绘制光标
  19. POINT ptPoint = { 0 };
  20. ::GetCursorPos(&ptPoint);
  21. ::DrawIconEx(hDCMem, ptPoint.x, ptPoint.y, hCursor, 0, 0, 0, NULL, DI_NORMAL | DI_COMPAT | DI_DEFAULTSIZE);
  22. // 定义位图信息头
  23. BITMAP stBmp = { 0 };
  24. ::GetObject(hDesktopBmp, sizeof(BITMAP), &stBmp);
  25. BITMAPINFOHEADER bmpInfoHeader = { 0 };
  26. ::RtlZeroMemory(&bmpInfoHeader, sizeof(BITMAPINFOHEADER));
  27. bmpInfoHeader.biSize = sizeof(BITMAPINFOHEADER);
  28. bmpInfoHeader.biWidth = iScreenWidth;
  29. bmpInfoHeader.biHeight = iScreenHeight;
  30. bmpInfoHeader.biSizeImage = stBmp.bmWidthBytes * iScreenHeight;
  31. bmpInfoHeader.biPlanes = stBmp.bmPlanes;
  32. bmpInfoHeader.biBitCount = stBmp.bmBitsPixel;
  33. bmpInfoHeader.biCompression = BI_RGB;
  34. // 获取桌面屏幕位图的DIB数据
  35. BYTE *lpBits = new BYTE[bmpInfoHeader.biSizeImage];
  36. BITMAPINFO bmpInfo = { 0 };
  37. bmpInfo.bmiHeader = bmpInfoHeader;
  38. iRet = ::GetDIBits(hDCSource, hDesktopBmp, 0, iScreenHeight, lpBits, &bmpInfo, DIB_RGB_COLORS);
  39. if (0 == iRet || NULL == lpBits)
  40. {
  41. ::MessageBox(NULL, "GetDIBits Error!", "ERROR", MB_OK | MB_ICONERROR);
  42. }
  43. // 添加视频帧到已创建的avi视频文件
  44. hr = AddAviFrame(pAvi, bmpInfoHeader, lpBits);
  45. if (S_OK != hr)
  46. {
  47. ::MessageBox(NULL, "AddAviFrame Error!", "ERROR", MB_OK | MB_ICONERROR);
  48. }
  49. // 释放
  50. ::SelectObject(hDCMem, oldBmp);
  51. delete[]lpBits;
  52. lpBits = NULL;
  53. ::DeleteObject(hDesktopBmp);
  54. ::DeleteObject(hCursor);
  55. ::DeleteDC(hDCMem);
  56. ::ReleaseDC(::GetDesktopWindow(), hDCSource);
  57. }

程序测试

我们运行程序,便开始录屏,等待一会儿后,停止录屏。这时,成功生成视频文件。打开视频,成功播放刚才的录制画面。

总结

注意,如果电脑上没有安装 XVID 解码器的话,录制视频的时候,就不能将画面进行压缩,因为我们程序里面指定使用 XVID 来压缩视频,获取压缩视频流的。所以,就会得到未压缩的视频文件,这时就会发现生成的视频文件比较大。

参考

参考自《Windows黑客编程技术详解》一书

上传的附件 cloud_download ScreenRecord_Test.7z ( 148.21kb, 42次下载 )

发送私信

如果没有为想要的东西努力,就别为自己失去的东西哭泣

16
文章数
16
评论数
最近文章
eject