原文
* f6 n' \6 e8 C( V8 P5 Lhttp://blog.csdn.net/matlab2000/archive/2006/10/30/1356752.aspx
4 D- x, }- D0 }
0 M7 D/ X% p3 o 常常我们想要编程捕获整个屏幕的内容,下面将解释如何做到这一点。典型的,我们的选择是使用GDI或者DirectX。另外一个值得考虑的方法是Windows Media API,直接将捕获的屏幕图像编码成码流输出,不过本文中不讨论该方法,主要讨论前两种技术。每一种方法中,我们得到了屏幕快照后,就可以用于保存或者编码输出。下面讨论了两种方法,并给出程序的部分代码和运行截图。0 Z% O% Z& x4 ]- S) O, c
! g( b! S8 K7 o$ b; C" E0 y
I/ i2 E6 Q, J% {% e- B
: @/ ?! P2 t% ^: C+ w3 t用GDI方法捕获. D( [3 U/ U- S
, X/ q+ K; m/ M
如果性能不是问题,并且我们需要的只是屏幕快照,就可以考虑采用GDI方法。这种方法基于桌面也是一个窗口的基本思想(桌面也有窗口句柄和设备上下文),只要得到被捕获桌面的上下文,就可以用普通的方法把内容blit到我们应用定义的上下文中。如果知道桌面句柄(可以用函数GetDesktopWindow()函数得到),得到上下文是很直接的。步骤如下:- ?" f9 [7 N! b2 P5 s
; [5 @& D6 H3 S, h6 b
1) 使用GetDesktopWindow获得桌面句柄
2 l0 t" E$ p+ \! U7 u( N; G( v* o, Z/ w1 K! H( [+ K
2) 使用函数GetDC获得桌面窗口的DC
: [" A+ o5 j% Q0 V) [) C6 P( D$ e
0 l3 x z. k* n, M) n/ O% d5 M/ \ C3) 为桌面DC创建一个兼容的DC和一个用于选入兼容DC的兼容位图。可以用函数CreateCompatibleDC和CreateCompatibleBitmap;可以用SelectObject将位图选入我们的DC
! r6 m: H1 f& I g @1 [: E3 C* ~0 x1 g! C" I$ [% V& E6 U
4) 不管何时准备捕获屏幕,blit桌面DC内容到创建的兼容的DC,我们创建的兼容位图现在包含了捕获时刻的屏幕内容
" G& C$ z; T( J) e# V. Y5 ?0 j; t" C( G- j
5) 当完成捕获后,不要忘记释放对象
! ]" }8 U9 J; i( y, L Y3 q2 A, m4 r
例子:
4 E; t2 ^: y* b, n
3 G( x, ^" M" ?6 J; N7 eVoid CaptureScreen()
) J5 R6 [. c4 l8 J{
$ Z7 U& T* }3 I3 G8 x/ Y5 K( R3 Z int nScreenWidth = GetSystemMetrics(SM_CXSCREEN);
' ^+ I" A" {$ Z, ? int nScreenHeight = GetSystemMetrics(SM_CYSCREEN); Z A8 k- g4 f" m L
HWND hDesktopWnd = GetDesktopWindow();
$ e5 D+ s0 G5 R6 @ HDC hDesktopDC = GetDC(hDesktopWnd);; }8 X1 ?8 ]" z3 Q1 Q. E9 Y
HDC hCaptureDC = CreateCompatibleDC(hDesktopDC);
. w7 u5 s# g0 | HBITMAP hCaptureBitmap =CreateCompatibleBitmap(hDesktopDC, 4 Z" P0 j4 H: Z# M
nScreenWidth, nScreenHeight);
, i. U/ I+ x6 w SelectObject(hCaptureDC,hCaptureBitmap); ; ?% J5 e$ m/ h! c" N4 V
BitBlt(hCaptureDC,0,0,nScreenWidth,nScreenHeight,% }$ j$ ?' Y) U, y5 a N T
hDesktopDC,0,0,SRCCOPY|CAPTUREBLT); ) W2 }/ d0 y# [# {- M
SaveCapturedBitmap(hCaptureBitmap); //占位符号,可以在此处放入自己的代码
' `' F L0 m1 o ReleaseDC(hDesktopWnd,hDesktopDC);
: E( X! S( R1 a+ Y DeleteDC(hCaptureDC);) L& _9 i9 z% Y' T$ x' P
DeleteObject(hCaptureBitmap);
; A1 C. F; T3 I1 m3 A3 v/ X; g}; B; b/ r7 J- }4 N4 V
上面的例子中,函数GetSystemMetrics()如果使用SM_CXSCREEN则返回屏幕宽,如果使用SM_CYSCREEN则返回屏幕的高。参考相关代码细节,我们很容易创建文件或者影片。
4 G& \$ R7 a y- a8 C( a4 V8 ~' m8 Q! B( s
. i) I- I( E% ? N' k. k4 V& U9 p
, R1 _ d! V$ ^采用DirectX的方法:
0 f; p! ~( l- v7 `: ~
@2 }* Q9 `" @$ r* S/ _9 k 由于DirectX提供了一个干净的方法,所以捕获屏幕快照在DirectX下非常容易。% v9 ]" K, a' _# g6 z3 v1 B
* b: ?, }, o1 K- | t6 Y
每一个DirectX应用都包含了我们称为buffer或者surface的包含了相应于应用的视频内存,被称为后备缓冲。一些程序可能有超过一个后备缓冲。另外有一个所有程序能够存取的前端缓冲,该前端缓冲包含了桌面内容相关的视频内存,所以就是屏幕图像。! d7 l# {% i1 y. k, R
- o: z! P3 L. w; I& Y# p5 k8 }/ z, M L. j2 _/ A& h, @( k* W
* o; e9 x6 O& }6 h
通过存取我们应用程序的前端缓冲,我们就可以捕获此刻的屏幕内容。存取应用的前端缓冲是简单直接的。接口IDirect3DDevice9提供了GetFrontBufferData()方法,带一个IDirect3DSurface9对象指针,拷贝前端缓冲的数据进入surface. IDirect3DSurfce9对象能够通过方法IDirect3DDevice8::CreateOffscreenPlainSurface()获得。一旦屏幕被捕获到surface,我们可以使用D3DXSaveSurfaceToFile()以位图方式保存表面到磁盘。捕获代码如下:7 B A) L: {# p1 J
4 s# B: I) B3 `0 Oextern IDirect3DDevice9* g_pd3dDevice;
9 P0 e( i% x' X9 Y4 I7 kVoid CaptureScreen()" _3 v; R- q9 E! @- `# W2 f- N$ C
{
& D) \' c, a9 {* B% v, m Y IDirect3DSurface9* pSurface;
; L. j- \# ]6 P' Z1 l g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,1 t8 J" U0 d+ Z' @- x3 @- ^" U7 I+ K
D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &pSurface, NULL);
& W: {- N" f1 Y. F: N8 W g_pd3dDevice->GetFrontBufferData(0, pSurface);
0 x+ ^/ M5 k& u D3DXSaveSurfaceToFile("Desktop.bmp",D3DXIFF_BMP,pSurface,NULL,NULL); N* Y% E- Q! n. }/ {0 D$ N$ z
pSurface->Release();
' R" v9 y: }, H: `& H9 M+ m! R}
9 N" I( g8 @4 t% Y$ k上面代码中的g_pd3dDevice是一个IDirect3DDevice9对象,假定已经正确的初始化了。代码片断直接保存捕获图像到磁盘。然而,代替保存到磁盘,我们可能想要直接操作这个位图。我们可以通过使用方法IDirect3DSurface9::LockRect()来做到这点,获得一个指向表面的指针。我们可以拷贝图像到应用内存然后操作他们。接下来的代码片断表明了如何拷贝表面到我们的应用内存:
S6 B3 h. v5 W6 h& L1 T T% Q3 y5 U( L/ D& r
extern void* pBits;' q! k. K- K0 V
extern IDirect3DDevice9* g_pd3dDevice;$ w r0 f4 m1 A; ~ c( i
IDirect3DSurface9* pSurface;
" P+ f. U9 u# P* @2 }2 l. k5 Fg_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,
5 I3 Y% a- h0 L. v8 v' P7 Y9 b% Z D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, % i7 Y n% D1 z& b% f& U! N. A% `
&pSurface, NULL);% x% ^" y+ P2 E6 {
g_pd3dDevice->GetFrontBufferData(0, pSurface);
# J: A; c7 s% A( \D3DLOCKED_RECT lockedRect;, a5 s& L! ]8 q3 h- k1 [2 {' x
pSurface->LockRect(&lockedRect,NULL,! _: V/ u) {8 I6 ~
D3DLOCK_NO_DIRTY_UPDATE|/ D. w2 }/ g* `0 R
D3DLOCK_NOSYSLOCK|D3DLOCK_READONLY)));
& b: e, J" y% wfor( int i=0 ; i < ScreenHeight ; i++)
6 w" I; c7 e) W. e8 q# K% t{/ \# a& Q% o4 g! e* L" i4 [
memcpy( (BYTE*) pBits + i * ScreenWidth * BITSPERPIXEL / 8 ,
/ p; a- |4 O% n/ s& [ s6 H (BYTE*) lockedRect.pBits + i* lockedRect.Pitch , ' D- E3 z9 H; S
ScreenWidth * BITSPERPIXEL / 8);6 g6 U- C- h( T3 E
}
' ?5 w5 d: E s7 ^0 v% Pg_pSurface->UnlockRect();. w6 ]+ q) z+ A; X$ i; H
pSurface->Release();
& J+ A+ g% D0 ^# ~* x% ]& D ( x% s: J% n; ^/ l
7 ?# L K7 N0 f9 d) A' L) O& d2 C7 {0 N
上面的pBits是一个void *,在拷贝之前要确认我们已经分配了足够的内存。一个BITSPERPIXEL的典型值是32位,然后,依赖于你的显示器设置。重要的是注意到表面的宽度与捕获屏幕的宽度并不相同,由于这个问题涉及到内存对齐,表面可能加入附加的部分到每行的结束来对齐到字边界。lockedRect.Pitch给出两个行之间的字节数目,也就是说,为了前进到下一行正确的点,我们应当推进Pitch,而不是Width.下面的代码反向的拷贝表面的字节:
, s; ]6 K+ _3 j2 C; Q0 h" l5 N$ X* T" F
for( int i=0 ; i < ScreenHeight ; i++)7 g) e3 f: i1 Y, k
{
# }7 g$ a+ T# Z! i' i) U% Z" d; m z memcpy((BYTE*) pBits +( ScreenHeight - i - 1) *
# `# b+ V) a& P& ?/ Y: z8 x- W9 m/ O ScreenWidth * BITSPERPIXEL/8 ,
' b. L! e5 P. ~2 X2 ` (BYTE*) lockedRect.pBits + i* lockedRect.Pitch , # m8 Q N" u; g. _
ScreenWidth* BITSPERPIXEL/8);
+ l3 ^7 v: K3 N6 x}/ \( V; M$ v2 n* B4 ?3 O0 D% P" r
& [9 D, Q6 e6 d; @' @8 n0 j
# n0 |7 ^( j8 j, v这点代码用于转换自顶而下和自底而上的位图。3 u b; X* ~6 F$ D& Z
3 l# ]- a, K7 d; e3 @
上面的技术中LockRect是IDirect3DSurface9中存取捕获图像的一个方法,我们还有另外一个更加复杂的方法,GetDC方法。IDirect3DSurface9::GetDC()方法用来获得GDI兼容的设备上下文,这样就可以直接blit表面内容到我们应用定义的DC。
2 m/ L+ Z$ i" _+ t, q6 f
2 o# E7 [: Y# k ( Y4 l( u1 g+ |% R8 R
: c k: U- R2 C, }7 s
然而,GetFrontBufferData是一个慢的方法,不应当用于性能临界的应用,GDI方法是更快的方法。
+ J1 d1 ^5 i: G! ?: Q9 j. b+ w' a" c
0 _3 j3 |( u* [8 } G/ r& [, z0 p! {2 X% W' ~% U, [
捕获程序要点:
% r( \) d! J, X) J1 u% ]! l
" D" Z1 @% W/ s# k5 B# K 程序初始化的时候需要初始化相关资源,在WM_CREATE消息中初始化D3D,然后初始化FFMPEG,同时启动定时器;当用户选择开始捕获的时候,每次定时器到,处理WM_TIMER消息,锁定前端缓冲平面,然后拷贝数据到缓冲区,将该缓冲区传递给FFMPEG编码并输出;当用户停止的时候,停止定时器,清理D3D和FFMPEG,然后退出。下面是几个需要注意的技术点:1 s! }6 o% R$ a; _5 _
& o5 X+ {! K6 Y! a4 c1)屏幕的颜色深度,由于屏幕可能处于不同的颜色深度下,所以需要仔细区分,当GetDeviceCaps函数返回颜色深度为16的时候,需要区分是否RGB555或者RGB565,根据颜色深度来设定传递给FFMPEG的像素格式。
# g/ @" `* F3 ]7 ^! C" ^- J* V/ ?# [# H E, H0 f& m
2)编码参数的设定,由于编码库支持的图像大小不是任意的,所以需要仔细的设置编码参数,否则不能初始化编码器。5 x$ [) f8 \& Z# ~' F0 }* X& E
: I* B! s ^& q! r, S# w3)图像转换的设定,由于原图像和目的图像的色彩空间和大小都不一样,所以需要仔细考虑图像转换,色彩空间的转换可以用img_convert,大小的转换可以用img_resample,注意到大小转换只针对YUV420P,所以在本案例中是先转换色彩空间,从RGBA32转换到YUV420P,然后调用重采样函数改变大小。
+ B5 S9 Z6 f! P7 g- [8 a8 b8 ^1 T/ f, l; a
4)编码图像的保存,调用avcodec_encode_video编码图像,然后直接写入文件即可,如果想要从网络输出,那么考虑rtp_callback函数回调,由库来做合适的分包,然后加上合适的头部发送: n5 l5 W2 R3 p# B! @. u- {
, z& |- K2 U/ i' f# k( c5)抓屏过程中,程序需要转换成托盘图标。保存为bmp的时候图像行是颠倒的,要把表面的第一行作为bmp的最后一样。传给解码器的是正常坐标2 h2 J/ d8 B! f
' Z5 }/ w$ u! x e ) ^$ M2 o5 e5 Q! {
6 [% N; V# {. s! H程序运行截图:
0 y8 r7 V, ^2 m6 c/ m. \
; h: n1 e2 n$ w4 ~' z: x9 W& G u% v, j% x6 Q
该图是采用的捕获工具所截获的H263纯码流通过VLC播放然后用快照功能获得,是CIF的分辨率。* |1 ?2 e+ r, [- ]
7 n9 H: E- h! A: y6 a
一些可能的改进:% x! |+ B- \! f7 z4 s: m* |) K
4 v U7 t& d5 `$ f
1.针对DirectX和OpenGL的窗口捕获,这个需要采用hook技术,比较麻烦6 w7 T' l5 w: ]# d% ~ @& ]& s. W
' r" s+ c1 V& u+ c& D
2.针对不同图像尺寸的编码,目前的库不能支持XGA的H263编码,4cif的编码出现了不规则捕获的问题,问题还没有查清楚,也许是能力问题,也许是程序设计问题。! s% x" X; j% l; {; i
9 K! D$ j0 L) m7 n# | M+ z3.编程中的一些优化和保护措施没有做好,程序运行的时候占用的CPU资源比较大 |