1. 视频流捕获机制
1.1. 视频流捕获基本原理
所谓视频流,实际上是由一张张图像组成,由于人体眼睛的捕获频率,以及视觉暂留机制,在图像连续播放时,会让大脑以为产生连贯性的动画效果。常见的电影帧率是24帧/秒(24fps),而游戏一般要到60fps(高清)才不会觉得明显卡顿(这个讨论见https://www.zhihu.com/question/21081976/answer/34748080)。也就是,对于电影或者一般游戏而言,按照某个帧率播放,只要按照这个帧率捕获数据,基本可以采集到每一帧完整的数据。
再来一个直观的感受:
- 帧率在24fps,播放一张图的时间是41.7ms,如果1s播放一张图,那么帧率在1fps。
- 帧率在30fps,播放一张图的时间是33.3ms
- 帧率在60fps,播放一张图的时间是16.7ms
1.2. 窗口图像帧捕获
1.2.1. 基本原理
为了取得窗口图像帧,首先得清楚窗口图像帧原理。
大概的原理:
Windows为了提升显示性能,图形相关的操作,都在内核层(win32k.sys),在这里,设计上有两条线:
- 一条是管理线,在win32k.sys中有专门的窗口管理器,处理窗口的父子层级关系、foreground窗口、焦点信息、各窗口句柄,以及各个消息队列;
- 一条是渲染线,这里win32k应该是提供了与显示设备驱动交互的功能,通过GDI接口对外暴露出来。应用程序通过调用GDI接口,对窗口绑定的显示缓存进行绘制。最终还是结合管理线中的窗口层级,来做显示的消隐处理。
应用层使用的基本方式,就是创建消息循环,创建窗口及绑定窗口过程函数,通过消息循环派发消息事件给过程函数,去执行不同动作。渲染线最后是通过WM_PAINT消息驱动,去刷新每个窗口自身的显示区,最终触发屏幕对应的FrontBuffer数据展示。这里的每个窗口自身的显示区,windows中用DC来关联,每一个窗口都有一个DC以及一段buffer,实际的操作,都是通过DC来处理这段buffer。我们可以通过DC来获取到窗口内的数据。
窗口管理器负责管理了各个窗口的显示资源buffer地址,应用程序的渲染过程负责对窗口显示buffer资源进行修改。那么获取图像帧,只需要拿到最终修改完成的buffer即可,可通过DC操作来拿。
1.2.2. 窗口捕获代码
void SaveBitmapToFile(HBITMAP hBitMap, LPCWSTR lpstrFileName)
{
BITMAP bitmap;
GetObject(hBitMap, sizeof(BITMAP), &bitmap);
BITMAPFILEHEADER bmfHdr; //位图文件头结构
BITMAPINFOHEADER bi; //位图信息头结构
LPBITMAPINFOHEADER lpbi; //指向位图信息头结构
bi.biSize = sizeof(BITMAPINFOHEADER);
bi.biWidth = bitmap.bmWidth;
bi.biHeight = bitmap.bmHeight;
bi.biPlanes = 1;
HDC hDC = CreateDC(L"DISPLAY", NULL, NULL, NULL);
int iBits = GetDeviceCaps(hDC, BITSPIXEL) * GetDeviceCaps(hDC, PLANES);
DeleteDC(hDC);
if (iBits <= 1) bi.biBitCount = 1;
else if (iBits <= 4) bi.biBitCount = 4;
else if (iBits <= 8) bi.biBitCount = 8;
else if (iBits <= 24) bi.biBitCount = 24;
else bi.biBitCount = iBits;
bi.biCompression = BI_RGB;
bi.biSizeImage = 0;
bi.biXPelsPerMeter = 0;
bi.biYPelsPerMeter = 0;
bi.biClrUsed = 0;
bi.biClrImportant = 0;
DWORD dwBmBitsSize = ((bitmap.bmWidth * bi.biBitCount + 31) / 32) * 4 * bitmap.bmHeight;
// 计算调色板大小
DWORD dwPaletteSize = 0;
if (bi.biBitCount <= 8) dwPaletteSize = (1 << bi.biBitCount) * sizeof(RGBQUAD);
// 设置位图文件头
bmfHdr.bfType = 0x4D42; // "BM "
DWORD dwDIBSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dwPaletteSize + dwBmBitsSize;
bmfHdr.bfSize = dwDIBSize;
bmfHdr.bfReserved1 = 0;
bmfHdr.bfReserved2 = 0;
bmfHdr.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER) + dwPaletteSize;
// 为位图内容分配内存
HANDLE hDib = GlobalAlloc(GHND, dwBmBitsSize + dwPaletteSize + sizeof(BITMAPINFOHEADER));
lpbi = (LPBITMAPINFOHEADER)GlobalLock(hDib);
*lpbi = bi;
// 处理调色板
HPALETTE hPal = (HPALETTE)GetStockObject(DEFAULT_PALETTE);
HPALETTE hOldPal = NULL;
if (hPal)
{
hDC = GetDC(NULL);
hOldPal = SelectPalette(hDC, hPal, FALSE);
RealizePalette(hDC);
}
// 获取该调色板下新的像素值
GetDIBits(hDC, hBitMap, 0, (UINT)bitmap.bmHeight, (LPSTR)lpbi + sizeof(BITMAPINFOHEADER) + dwPaletteSize, (LPBITMAPINFO)lpbi, DIB_RGB_COLORS);
// 恢复调色板
if (hOldPal)
{
SelectPalette(hDC, hOldPal, TRUE);
RealizePalette(hDC);
ReleaseDC(NULL, hDC);
}
HANDLE hFile = CreateFile(lpstrFileName, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
// 写入位图文件头
DWORD dwWritten = 0;
WriteFile(hFile, (LPSTR)&bmfHdr, sizeof(BITMAPFILEHEADER), &dwWritten, NULL);
// 写入位图文件其余内容
WriteFile(hFile, (LPSTR)lpbi, dwDIBSize, &dwWritten, NULL);
GlobalUnlock(hDib);
GlobalFree(hDib);
CloseHandle(hFile);
}
void Capture(HWND hWnd)
{
HDC hdc = GetDC(hWnd);
RECT rcWnd = {
0 };
GetClientRect(hWnd, &rcWnd);
int cx = rcWnd.right - rcWnd.left;
int cy = rcWnd.bottom - rcWnd.top;
HDC hdcMem = CreateCompatibleDC(hdc);
HBITMAP bitmap = CreateCompatibleBitmap(hdc, cx, cy)