Windows下最快的屏幕截图方法

183

我想为Windows平台编写一个屏幕录制程序,但不确定如何捕捉屏幕。我所知道的唯一方法是使用GDI,但我想知道是否有其他方法可以达到同样的效果,并且如果有的话,哪种方法的开销最小?速度是首要考虑因素。

这个屏幕录制程序将用于记录游戏画面,但如果这会限制选项,我仍然对任何超出此范围的建议持开放态度。毕竟,没有坏处的知识。

编辑:我看到了这篇文章:各种捕获屏幕的方法。它介绍了使用Windows Media API和DirectX的方法。在结论中提到禁用硬件加速可以大大提高录制应用程序的性能。我想知道为什么会这样。有人可以为我填补这些空白吗?

编辑:我了解到像Camtasia这样的屏幕录制程序使用自己的捕获驱动程序。有人可以详细解释一下它是如何工作的以及为什么更快吗?我可能还需要指导如何实现类似的东西,但我相信已经存在相关的文档。

此外,我现在知道FRAPS如何录制屏幕。它挂钩底层图形API以从后台缓冲区读取。据我所知,这比从前台缓冲区读取要快,因为您正在从系统RAM而不是视频RAM中进行读取。您可以在此处阅读该文章。


2
你不需要hook任何东西。你只需要编写输入事件,以便它们不直接控制游戏,而是调用其他函数。例如,如果玩家按下左键,您不应该简单地减少玩家的x位置。相反,您需要调用一个函数,比如MovePlayerLeft()。同时,您还需要记录按键和其他输入的时间和持续时间。然后,当您处于回放模式时,您只需忽略输入,而是读取记录的数据。如果在数据中看到左键按下,则调用MovePlayerLeft() - Benjamin Lindley
2
@PigBen 这将是一个通用的应用程序,用于记录游戏画面。它不是针对特定游戏的。我知道,有人按左键可能意味着向右移动。此外,您还没有考虑到不受用户影响的事件。“那渲染呢?” - someguy
1
@someguy 好的,我猜你正在做更加复杂的事情。我使用上述方法添加了一个例程,在游戏中以约30fps保存回放AVI文件而没有任何问题。我使用Windows API制作了一个多监视器屏幕录像机,以进行“工作力量优化”,但即使以4fps为目标,性能也很差。 - AJG85
1
在UltraVNC的代码库网站上有一个Windows开源镜像驱动程序,位于此处http://ultravnc.svn.sourceforge.net/viewvc/ultravnc/UltraVNC%20Project%20Root/UltraVNC/winvnc/winvnc/。 - Beached
1
@anddero: 正确。我没有机会对任何方法进行基准测试。我怀疑我很快就不会尝试了,因为我放弃了这个项目。如果你能自己尝试并报告一些发现,那将是很棒的。 - someguy
显示剩余14条评论
16个回答

66

这是我用来收集单帧的代码,但如果您修改它并保持两个目标一直打开,则可以使用静态计数器将其“流式传输”到磁盘上,以用作文件名。- 我不记得在哪里找到它,但它已被修改,感谢那些做出贡献的人!

void dump_buffer()
{
   IDirect3DSurface9* pRenderTarget=NULL;
   IDirect3DSurface9* pDestTarget=NULL;
     const char file[] = "Pickture.bmp";
   // sanity checks.
   if (Device == NULL)
      return;

   // get the render target surface.
   HRESULT hr = Device->GetRenderTarget(0, &pRenderTarget);
   // get the current adapter display mode.
   //hr = pDirect3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT,&d3ddisplaymode);

   // create a destination surface.
   hr = Device->CreateOffscreenPlainSurface(DisplayMde.Width,
                         DisplayMde.Height,
                         DisplayMde.Format,
                         D3DPOOL_SYSTEMMEM,
                         &pDestTarget,
                         NULL);
   //copy the render target to the destination surface.
   hr = Device->GetRenderTargetData(pRenderTarget, pDestTarget);
   //save its contents to a bitmap file.
   hr = D3DXSaveSurfaceToFile(file,
                              D3DXIFF_BMP,
                              pDestTarget,
                              NULL,
                              NULL);

   // clean up.
   pRenderTarget->Release();
   pDestTarget->Release();
}

谢谢。我听说过一种比从前缓冲区读取更快的方法。你真的是这样做的吗?它能正常工作吗? - someguy
前缓冲区的问题在于访问,也就是说,尝试复制当前正在渲染的平面会“中断”复制。这对我来说已经足够好了,但它会占用我的硬盘空间! - Brandrew
1
我就是无法让它正常工作... DirectX 在 GetRenderTargetData 部分报告了一些无效的调用。显然,你创建设备的方式非常重要。 - LightStriker
API调用对我来说很好。然而,我得到的只是一张完全黑色的图像。我错过了什么吗?这种方法真的有效吗? - Hrishikesh_Pardeshi
19
Downvote 只适用于您自己的应用程序,因此无法用于记录通用程序。 - user3125280
显示剩余7条评论

45

编辑:我可以看到这被列在你的第一个编辑链接下,称为“GDI方式”。即使在该网站上有性能建议,这仍然是一种不错的选择,我认为你可以轻松达到30fps。

来自this评论(我没有做过这个,我只是参考了别人的经验):

HDC hdc = GetDC(NULL); // get the desktop device context
HDC hDest = CreateCompatibleDC(hdc); // create a device context to use yourself

// get the height and width of the screen
int height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
int width = GetSystemMetrics(SM_CXVIRTUALSCREEN);

// create a bitmap
HBITMAP hbDesktop = CreateCompatibleBitmap( hdc, width, height);

// use the previously created device context with the bitmap
SelectObject(hDest, hbDesktop);

// copy from the desktop device context to the bitmap device context
// call this once per 'frame'
BitBlt(hDest, 0,0, width, height, hdc, 0, 0, SRCCOPY);

// after the recording is done, release the desktop context you got..
ReleaseDC(NULL, hdc);

// ..delete the bitmap you were using to capture frames..
DeleteObject(hbDesktop);

// ..and delete the context you created
DeleteDC(hDest);

我并不是说这是最快的方法,但如果你在兼容的设备上下文之间复制,BitBlt 操作通常非常快。

作为参考,Open Broadcaster Software 将类似于此的内容作为其 "dc_capture" 方法的一部分实现,尽管他们使用 IDXGISurface1 创建目标上下文 hDest,该方法适用于 DirectX 10+。如果不支持此功能,则会回退到 CreateCompatibleDC

要将其更改为使用特定应用程序,您需要将第一行更改为 GetDC(game),其中 game 是游戏窗口的句柄,然后设置正确的游戏窗口的 heightwidth

一旦您在hDest/hbDesktop中获得了像素,仍然需要将其保存到文件中,但如果您正在进行屏幕捕获,则应该想要在内存中缓冲一定数量的像素,并分块保存到视频文件中,因此我不会指向保存静态图像到磁盘的代码。


3
摘录:如果源设备环境和目标设备环境的颜色格式不匹配,BitBlt函数将转换源颜色格式以匹配目标格式。 - user244343
2
有一些证据来证实这将是很好的。你是否在某个地方发布了性能比较,或者看到了一个准确的比较? - user244343
2
尝试对其进行剖析。正如我在帖子中所说,我正在参考一些具有 GDI 经验的人。如果它泄漏内存,并且您知道如何修复它,请编辑帖子以消除泄漏。 - user244343
1
我用这种方法只得到了大约5帧每秒的速度。这是在一台集成显卡的笔记本电脑上测试的,所以我期望在一台配备真正独立显卡的台式机上会有更好的表现,但仍然非常慢。 - Timmmm
1
@Timmmm,我已经添加了对OBS实现方式的引用。希望这能为您加快速度。 - user244343
显示剩余5条评论

24

我编写了一个视频捕获软件,类似于针对DirectX应用程序的FRAPS。源代码可用,我的文章解释了一般技术,请参考http://blog.nektra.com/main/2013/07/23/instrumenting-direct3d-applications-to-capture-video-and-calculate-frames-per-second/.

关于性能相关的问题,

  • DirectX应该比GDI更快,除非你正在从非常慢的前缓冲区读取。我的方法类似于FRAPS(从后缓冲区读取)。我截取了一组来自Direct3D接口的方法。

  • 对于实时视频录制(最少影响应用程序),快速的编解码器是必不可少的。FRAPS使用自己的无损视频编解码器。Lagarith和HUFFYUV是专为实时应用程序设计的通用无损视频编解码器。如果想要输出视频文件,您应该查看它们。

  • 另一种记录屏幕广播的方法是编写镜像驱动程序。根据维基百科:当视频镜像处于活动状态时,每次系统在镜像区域内的主要视频设备上绘制时,都会在镜像视频设备上实时执行绘制操作的副本。参见MSDN上的镜像驱动程序:http://msdn.microsoft.com/en-us/library/windows/hardware/ff568315(v=vs.85).aspx


18

我使用d3d9获取后备缓冲区,并使用d3dx库将其保存为png文件:

    IDirect3DSurface9 *surface ;
// GetBackBuffer idirect3ddevice9->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &surface ) ;
// 保存表面 D3DXSaveSurfaceToFileA( "filename.png", D3DXIFF_PNG, surface, NULL, NULL ) ;
SAFE_RELEASE( surface ) ;

要做到这一点,您应该使用以下代码创建交换缓冲区:

d3dpps.SwapEffect = D3DSWAPEFFECT_COPY ; // for screenshots.

(这样你就可以保证在截取屏幕截图之前,后备缓冲区没有被损坏)。


有道理,谢谢。你知道这个和 GetRenderTarget 之间的区别吗? - someguy
这只是获取当前的渲染目标(如果有人在您调用时呈现到纹理,则可能是另一个离屏表面)。 - bobobobo

16
在我看来,使用GDI和DX的方法在本质上是不同的。使用GDI绘制时采用了FLUSH方法,这种方法先绘制帧,然后清除它并在同一缓冲区中重新绘制另一帧,这会导致需要高帧率的游戏出现闪烁。
为什么DX更快呢?在DX(或图形世界)中,应用了一种更成熟的方法,称为双缓冲区渲染。它有两个缓冲区,当将前面的缓冲区呈现给硬件时,你也可以渲染到另一个缓冲区,然后在完成第一帧渲染后,系统会切换到另一个缓冲区(将其锁定以呈现给硬件,并释放先前的缓冲区),通过这种方式,渲染效率得到了大大提高。
为什么关闭硬件加速更快呢?虽然使用双缓冲区渲染可以提高FPS,但是渲染时间仍然受限。现代图形硬件通常在渲染过程中涉及许多优化,通常像抗锯齿等需要大量计算,如果你不需要那么高质量的图形,当然可以禁用此选项,这将为你节省一些时间。
我认为你真正需要的是重放系统,这点我完全同意人们所讨论的。

1
请参阅讨论,了解为什么重放系统不可行。屏幕录制程序并不针对任何特定的游戏。 - someguy

13

我编写了一个实现GDI方法的屏幕捕获类。为了获取更快的速度,我尝试了DirectX方法(通过GetFrontBuffer),但我惊讶地发现GDI的性能大约快2.5倍。在进行100次捕获双显示器的测试后,GDI实现的平均每个屏幕捕获时间为0.65秒,而DirectX方法的平均时间为1.72秒。因此,根据我的测试结果,GDI显然比GetFrontBuffer更快。

我无法让Brandrew的代码工作来通过GetRenderTargetData测试DirectX。屏幕截图出来是纯黑色的。但是,它可以非常快速地复制那个空白屏幕!我将继续调试并希望得到一个可行的版本,以便看到真正的结果。


谢谢提供信息。我还没有测试Brandrew的代码,但我知道采用“GetRenderTargetData”方法是可行的。也许等我完成我的应用程序后,我会写自己的答案。或者,你在一切都正常工作后可以更新你的答案。 - someguy
每个屏幕截图 0.65 美元?一个良好的 GDI 实现(保持设备,等等)应该在现代计算机上轻松实现 1920x1200 的 30 帧。 - Christopher Oezbek
我认为GDI渲染的图像质量肯定比DX差。 - zinking
我已经使用SlimDX在C#中执行了这个测试,并惊讶地发现了相同的结果。也许这与使用SlimDX时需要为每个帧更新创建一个新的流和位图有关,而不是创建一次,倒回并继续覆盖相同位置。 - Cesar
1
只是澄清一下:实际上,“相同的结果”是指GDI更快 - 正如@Christopher所提到的,30fps +非常可行,仍然有足够的CPU剩余。 - Cesar
1
回顾过去,我觉得在我的基准测试中可能错误地包含了一些屏幕截图后的后处理。然而,如果是这样的话,我相信同样的后处理也被同时用于GDI和DX。因此,我仍然认为这是支持GDI作为更快方法的证据。 - rotanimod

12
你需要使用自 Windows 8 起可用的 Desktop Duplication API。这是官方推荐的方法,也是最高效的 CPU 方法。其中一个很好的屏幕录制功能是它可以检测窗口移动,因此当窗口移动时,您可以传输块增量而不是原始像素。此外,它会告诉您从一帧到下一帧哪些矩形已更改。微软的示例代码相当复杂,但 API 实际上很简单易用。我编写了一个示例项目,它要简单得多。WindowsDesktopDuplicationSample Desktop Duplication API Official example code(上面的示例是该代码的简化版本)。

这个Github项目在Windows 10上运行完美,已测试过网页视频和《生化危机7》。最好的是,CPU负载随着图形升级速度而缩放。 - jw_
一旦您启动GPU-Z,该程序就会停止抓取屏幕。 - Marino Šimić

11
对于C++,您可以使用:http://www.pinvoke.net/default.aspx/gdi32/BitBlt.html
这可能不能在所有类型的3D应用程序/视频应用程序上工作。然后此链接可能更有用,因为它描述了您可以使用的3种不同方法。

旧答案(C#):
您可以使用System.Drawing.Graphics.Copy,但速度不太快。

我编写了一个完全实现此功能的示例项目:http://blog.tedd.no/index.php/2010/08/16/c-image-analysis-auto-gaming-with-source/

我计划更新此示例,使用像Direct3D这样更快的方法:http://spazzarama.com/2009/02/07/screencapture-with-direct3d/

以下是有关捕获视频的链接:如何使用C# .Net捕获屏幕以成为视频?


3
啊,我忘了提到我正在使用 C(可能是 C++)编程,并且没有计划使用 .NET。非常抱歉 :/。 - someguy
我已经知道了BitBlt(GDI)。不过我会研究一下Direct3D。谢谢! - someguy
我之前几个星期有过一些了解,但还没有时间来实现它。Direct3D比使用GDI+的C#内置方法要快得多。 - Tedd Hansen
我已经更新了原始帖子。根据链接,当需要调用GetFrontBufferData()时,DirectX会变慢。在录制游戏画面时,这是否值得考虑?你能为我解释一下吗? - someguy
我还没有测试过它,所以我只能说出我记得读到的(最终决定采用)DirectX。我曾经在一篇好的博客文章上看到过相关内容,但现在我试图搜索时找不到了。抱歉。:/ - Tedd Hansen
1
GDI速度较慢,因此不适用于该问题域,DirectX或OpenGL将是唯一明智的建议。 - user206705

9
我能够提供的一些信息:显然使用"镜像驱动程序"是快速的,尽管我不知道是否有开源软件。此外,似乎使用StretchRect的某些卷积比BitBlt更快。

为什么RDP与其他远程控制软件相比如此之快?

http://betterlogic.com/roger/2010/07/fast-screen-capture/comment-page-1/#comment-5193

你提到的方法(fraps hooking into the D3D dll's) 可能是D3D应用程序中唯一的方法,但在Windows XP桌面捕获中不起作用。所以现在我只希望有一个类似于fraps速度快的普通桌面窗口... 有人吗?

(我认为使用aero可能可以使用类似于fraps的钩子,但XP用户将没有机会)。

此外,显卡位深度的更改和/或禁用硬件加速也可能有所帮助(和/或禁用aero)。

https://github.com/rdp/screen-capture-recorder-program包括一个相当快的基于BitBlt的捕获实用程序,以及一个基准测试程序,可让您对BitBlt速度进行基准测试以进行优化。

VirtualDub还具有一个“opengl”屏幕捕获模块,据说速度很快,并且可以执行诸如更改检测http://www.virtualdub.org/blog/pivot/entry.php?id=290


我在想,使用你的“屏幕捕获记录器”会更快,还是自己使用BitBlt会更快?你的项目中是否有一些优化呢? - blez
如果你自己使用 Bitblt,你可能会避免额外的 memcpy(memcpy 通常不是最大的瓶颈,尽管它确实会增加一些时间——我在这里只是提到它作为基准实用程序,但如果你需要某些东西或 directshow,那么它很好)。 - rogerdpack

6
您可以尝试使用C++开源项目WinRobot @git,它是一个功能强大的屏幕截图工具。
CComPtr<IWinRobotService> pService;
hr = pService.CoCreateInstance(__uuidof(ServiceHost) );

//get active console session
CComPtr<IUnknown> pUnk;
hr = pService->GetActiveConsoleSession(&pUnk);
CComQIPtr<IWinRobotSession> pSession = pUnk;

// capture screen
pUnk = 0;
hr = pSession->CreateScreenCapture(0,0,1280,800,&pUnk);

// get screen image data(with file mapping)
CComQIPtr<IScreenBufferStream> pBuffer = pUnk;

支持:

  • UAC窗口
  • Winlogon
  • DirectShow覆盖层

1
那是很多底层钩子。如果你有管理员权限,捕获速度是惊人的。 - toster-cx
我发现WinRobot非常强大而且流畅。 - ashishgupta_mca
我研究了WinRobot代码,对于屏幕截图方面没有看到什么突破性的东西:它使用的是相同的CreateCompatibleDC..BitBlt。除非在服务上下文中执行此操作时有一些魔法? - shekh
2
@shekh:你的研究太肤浅了。该代码使用IDirectDrawSurface7->BltFast()将屏幕从屏幕绘图表面复制到副本DD表面,然后使用文件映射来复制图像。这很复杂,因为代码正在服务中运行,你无法轻易访问桌面。 - Elmue

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接