Keen的博客

记录所思、所想、所遇

欢迎来到我的个人站~


【音视频】OBS原理分析

1. OBS介绍

OBS(Open Broadcaster Software,开放广播软件),免费的开源音视频软件,用于音视频记录和实时流媒体播放。
目前的OBS为obs studio版本,它是跨平台重构后的版本,对比原始版本obs classic。

obs studio版本代码位置:https://github.com/obsproject/obs-studio
obs classic版本代码位置:https://github.com/jp9000/OBS

2. OBS源码编译与安装

参考https://github.com/obsproject/obs-studio/wiki/Install-Instructions

需要注意的是:

3. 直播系统的本质

png

直播系统的本质(采集器 + 后台服务 + 播放器):

  • 主播采集摄像头数据、游戏画面数据、桌面窗口数据等,通过推流到达后端;
  • 后端对数据进行二次处理,录制缓存存储;
  • 观众通过播放器连接后端,获取音视频流数据。

OBS Studio这里作为采集端,所以主要关注的是音视频流的数据捕获。先来看视频流数据。

4. 视频流捕获机制

4.1. 视频流捕获基本原理

所谓视频流,实际上是由一张张图像组成,由于人体眼睛的捕获频率,以及视觉暂留机制,在图像连续播放时,会让大脑以为产生连贯性的动画效果。常见的电影帧率是24帧/秒(24fps),而游戏一般要到60fps(高清)才不会觉得明显卡顿(这个讨论见https://www.zhihu.com/question/21081976/answer/34748080)。也就是,对于电影或者一般游戏而言,按照某个帧率播放,只要按照这个帧率捕获数据,基本可以采集到每一帧完整的数据。

下图是摘抄至网上的关于帧率和码流率的一张对比图: jpg

再来一个直观的感受:

  • 帧率在24fps,播放一张图的时间是41.7ms,如果1s播放一张图,那么帧率在1fps。
  • 帧率在30fps,播放一张图的时间是33.3ms
  • 帧率在60fps,播放一张图的时间是16.7ms

4.2. 窗口图像帧捕获

4.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操作来拿。

4.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);
    SelectObject(hdcMem, bitmap);
    BitBlt(hdcMem, 0, 0, cx, cy, hdc, 0, 0, SRCCOPY);

    SaveBitmapToFile(bitmap, L"d:\\test.bmp");

    ReleaseDC(hWnd, hdc);
    DeleteDC(hdcMem);
    DeleteObject(bitmap);
}

核心是创建内存兼容DC,关联一个bitmap资源,然后BitBlt调用可以从显存dc拷贝到内存兼容dc,因为内存兼容dc与bitmap关联,数据就都到bitmap中去了,直接进行存储即可。

4.2.3. 窗口捕获测试

4.2.3.1. GDI窗口测试

被测试对象:

  • 一个常规GDI绘制窗口
  • 窗口内部简单加载了一张bitmap

测试窗口代码如下:

void Load(HINSTANCE hInstance, HWND hWnd)
{
    g_hBitMap = LoadBitmapW(hInstance, MAKEINTRESOURCE(IDB_BITMAP1));
    InvalidateRect(hWnd, NULL, TRUE);
}

void OnPaint(HDC hdc)
{
    HDC hdcMem = CreateCompatibleDC(NULL);
    SelectObject(hdcMem, g_hBitMap);
    BITMAP bm;
    GetObject(g_hBitMap, sizeof(bm), &bm);
    BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
    DeleteDC(hdcMem);
}

测试窗口:

png

捕获截图:

png

4.2.3.2. DX窗口应用测试

被测试对象:

  • 一个DX9渲染的窗口
  • 窗口内部简单绘制了一个三角形

dx9

IDirect3D9Ex* g_pD3D9Ex = NULL;
IDirect3DDevice9Ex* g_pD3D9DeviceEx = NULL;
IDirect3DVertexBuffer9* g_pVB = NULL;

struct CUSTOMVERTEX
{
	D3DXVECTOR4 position; // The position
	D3DCOLOR    color;    // The color
};

#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW|D3DFVF_DIFFUSE)

void InitDevice(HWND hWnd)
{
	HRESULT hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &g_pD3D9Ex);
	if (FAILED(hr)) return;

	D3DPRESENT_PARAMETERS d3dpp;
	ZeroMemory(&d3dpp, sizeof(d3dpp));
	d3dpp.Windowed = TRUE;
	d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
	d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
	d3dpp.hDeviceWindow = hWnd;
	if (FAILED(hr = g_pD3D9Ex->CreateDeviceEx(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
		D3DCREATE_SOFTWARE_VERTEXPROCESSING,
		&d3dpp, NULL, &g_pD3D9DeviceEx))) return;

	g_pD3D9DeviceEx->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
	g_pD3D9DeviceEx->SetRenderState(D3DRS_LIGHTING, FALSE);
	g_pD3D9DeviceEx->SetRenderState(D3DRS_ZENABLE, FALSE);

	// 定义顶点
	hr = g_pD3D9DeviceEx->CreateVertexBuffer(3 * sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &g_pVB, NULL);
	if (FAILED(hr)) return;
	CUSTOMVERTEX* pVertices;
	if (FAILED(g_pVB->Lock(0, 0, (void**)&pVertices, 0))) return;

	pVertices[0].position = D3DXVECTOR4(100.0f, 150.0f, 1.0f, 1.0f);
	pVertices[0].color = D3DCOLOR_XRGB(255, 0, 0);
	pVertices[1].position = D3DXVECTOR4(250.0f, 150.0f, 1.0f, 1.0f);
	pVertices[1].color = D3DCOLOR_XRGB(0, 255, 0);
	pVertices[2].position = D3DXVECTOR4(150.0f, 250.0f, 1.0f, 1.0f);
	pVertices[2].color = D3DCOLOR_XRGB(0, 0, 255);
	
	g_pVB->Unlock();
}

void Render()
{
	if (g_pD3D9DeviceEx && g_pVB)
	{
		g_pD3D9DeviceEx->Clear(0, 0, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 255), 1.0f, 0);

		if (SUCCEEDED(g_pD3D9DeviceEx->BeginScene()))
		{
			g_pD3D9DeviceEx->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));
			g_pD3D9DeviceEx->SetFVF(D3DFVF_CUSTOMVERTEX);
			g_pD3D9DeviceEx->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

			g_pD3D9DeviceEx->EndScene();
		}

		g_pD3D9DeviceEx->PresentEx(0, 0, 0, 0, 0);
	}
}

void UnInitDevice()
{
	if (g_pVB) g_pVB->Release();
	if (g_pD3D9DeviceEx) g_pD3D9DeviceEx->Release();
	if (g_pD3D9Ex) g_pD3D9Ex->Release();
}

测试窗口:

png

捕获结果:

png

4.2.4. 窗口捕获分析和总结

4.2.4.1. 窗口DC是什么

整个截图围绕着DC来操作,让我不禁思考,DC到底是什么?我们可以通过GetWindowDC以及GetDC获取窗口的两种不同设备上下文,但是当我对这个API重复调用时,发现返回的对象并不一样,如下调试结果:

png

为啥返回值不一样?难道DC不是窗口的一个固定的数据缓存么?

这里微软给出来解释:https://docs.microsoft.com/en-us/windows/win32/gdi/display-device-contexts
本质上,DC是一个系统内存中的结构体,这个结构体中,包含有:绘图所用的对象集合(例如:画笔pen、画刷brush、位图bitmap、调色板palette、字体font等)以及相关属性的集合,而显示位图bitmap信息,就是窗口的显示数据信息。

DC也分为好多类别,我们这里的DC指的是Display DC,而Display DC也分为好几类:

  • Common DC:大多数场景
  • Private DC:特殊DC,窗口类stype存在标记位CS_OWNDC的场景
  • Window DC:需要关注非客户区时使用
  • Parent DC:Child Window使用

这其中,Private DC对窗口DC中绘图对象的修改,会直接修改到模板,下一次GetDC时,会拿修改后的模板值填充新DC,直到窗口销毁(即使被主动ReleaseDC,修改也不会消失);而其他的DC,在Get到之后,通过SelectObject修改DC中的绘图对象,当ReleaseDC时,所有的修改都会丢弃,下一次GetDC依旧使用默认值填充绘图对象。

简单测试一下,使用CS_OWNDC之后的效果如下:

png

这里会发现,Private DC只作用于客户区DC,对于非客户区DC,不起作用。

为啥连续调用返回值不一样呢?这里就解释了,因为每次都是新生成一个DC资源,然后从老资源中拷贝到新资源中来,只不过Private DC有点特殊。微软还解释了,DC资源是非常宝贵的资源,系统内是有限的,如果我们存在DC泄漏,很可能造成系统GDI绘图异常,也就是导致系统内其他绘图程序下一次调用GetDC会失败。那既然DC是存在于内存中的,为啥数量还要设限制呢?

另外,在分析DX时,发现DX的surface接口中有个GetDC接口,是一个有限使用的功能接口,为了可以供GDI函数相互作用。那么,是否可以理解为,DX底层和GDI底层,都关联DC,DC也都关联窗口整个显示区,所以无论是DX窗口还是GDI窗口,都能通过DC去操作。这里又有个问题,DC在内存中,到底是显存映射的内存,还是非显存映射的系统内存?既然系统要限制使用数量,而且DX的Surface接口还与之关联(测试所使用的surface对象在显存中),并且通过DC截图可以捕获到无论是GDI渲染还是DX渲染的数据内容,那么是否可以理解成DC本质就是存储在显存中,只不过被映射到了系统内存(如果不是,那么我们能通过窗口的DC捕获到,只能说明,DX绘制的时候,会在系统内存拷贝一份镜像,而我们每次通过窗口DC去获取数据,都是从这个镜像中获取的,但是很明显,DX没有这么做,因为我们在渲染三角形时,使用了D3DPOOL_DEFAULT,它的资源只会放置在显存中,不会在系统内存中镜像一份。)

由此分析,得出的结论是,我们在GetDC时,应该是在系统内存中,映射出窗口的显存buffer,当我们使用BitBlt,应该是从映射内存拷贝输出到系统内存。

4.2.4.2. GDI和DX的关系

从上述GDI窗口的捕获结果,以及DX应用窗口的捕获结果来看,关联窗口的DC是整个窗口渲染的最终过程,也就是说,无论使用DX引擎如何渲染,最终展示效果,都是可以通过DC来捕获,那么GDI和DX他们的关系,应该是如下:

png

4.2.4.3. 存储分析

显示器显示一张图,原始尺寸大小,可以这么计算:分辨率颜色位。颜色位现在一般是32位(再高就没有太大意义,人眼无法分辨),这样计算的话,如果分辨率在1366768,那么一张图的大小将是:136676832/8=4MB,也就是,一张全屏的图片,没有任何压缩,就需要4MB存储空间,甚至包括一些比屏幕更大的窗口,占用空间就更大了,比如高清屏33602100分辨率,全屏窗口将占用空间:33602100*32/8=26.9MB,这个数据量还是相当大的,如果同时监控2个这样的窗口,就到达50MB。

4.2.4.4. 性能分析

无论是DX渲染的窗口,还是GDI渲染的窗口,最终输出的显示缓存,都可以由DC来获取。而获取的方式,主要是调用BitBlt来将显示数据不断拷贝到创建的内存。根据内存占用分析,每次BitBlt要拷贝这么大数据量,考虑效率的话,如果窗口展示的动画,显示很快(60fps),以上述高清屏为例(26.9MB),那么相当于1s需要拷贝26.9*60=1614MB,已经1.5GB的数据量了。那么,再考虑BitBlt传输速率,我们就可以得到一个拷贝帧数极限数值。

遗留问题:BitBlt的性能测试

BitBlt本质是将显示数据,从显存拷贝到内存。这里占用的系统资源,除了前面分析的存储空间之外,还有显存到内存到传输带宽。硬件连接上,显存是作为外设,介入到CPU系统中,一般通过PCIe总线连接。CPU如果想让内存和显存进行通信,所采用到机制,是MMIO,即内存映射IO,也就是将显存映射为系统的一段内存,进而CPU就可以操作显存资源了。但本质上,硬件连接还是分离的,数据拷贝操作,还得通过类似DMA直接存储机制,这里拷贝数据,还占用了这个带宽。当拷贝占用带宽导致PCIe总线忙时,也会影响其他渲染操作。

此外,BitBlt拷贝数据,还需要考虑是否有锁的问题,因为它是针对目标窗口的图像帧拷贝数据,而针对DC的操作,是否带锁?如果不带锁,对DC的操作则必须是单线程,否则容易导致数据混乱,多进程操作时,本质就是多线程在操作DC资源;如果带锁,那么只要使用它,应该就会对捕获目标造成影响。猜测的话,因为这个东西最终是需要占用PCIe总线的,至少在总线这里是独占的,可以理解为有锁,GDI操作自身是否有锁,这个有待逆向分析。

遗留问题:BitBlt逆向分析是否有锁??

由于我们是从进程外去拷贝窗口图像帧数据,所以是无法区分dirty空间大小,也就无法区分具体每次拷贝的大小,而每次都拷贝完整的全屏数据,这里很明显存在可以优化的空间。如果利用窗口捕获方式,可以注入到目标进程,在渲染时,获取到脏数据的位置,来拿到增量渲染数据,这样可以在采集时,降低DMA带宽的使用量。

4.2.4.5. 采样频率

由于我们在进程外获取窗口数据帧,我们实际上是不知道渲染何时触发,例如静态的窗口,可能一只不渲染,但是我们是很难区分开的,所以为了采集窗口数据帧,我们需要做的,是定时去做窗口数据采样。这样导致的问题,就是可能采样到一堆无用数据,对于我们无论是存储,还是网络传输,都是不利的(当然,对于网络传输,我们也可以分析图像差异来计算是否重复,但是这样只会导致消耗更多计算性能)。

如果我们注入到系统进程内,通过Hook绘制操作,那么便可以尽可能减少这种无效数据。

4.3. 游戏捕获

对于直播而言,游戏是一个重要分支,对于游戏自身而言,都是为了尽可能争取到高性能的渲染,而基本上采用DirectX、OpenGL、VulKan等与显卡紧密结合的渲染SDK进行游戏画面的渲染,游戏的帧率,也普遍非常之高。高清高速高性能的追求,对于数据采集而言,本就是一个挑战,如果采样前述的窗口捕获的方式,对于DMA带宽占用,本身就会影响当下游戏性能,而且游戏帧率随着场景变化,对于我们设置的采样间隔,会造成极大的浪费。所以,游戏这种捕获,OBS采用了独立功能和产品模块来实现捕获,以获取最佳性能。

为了能掌控渲染帧数的变化,实时获取图像帧数据,基本方式是:注入 + 绘制函数Hook。

4.3.1. DX渲染基本流程示例

参考https://keenjin.github.io/2020/04/DXRender/#3-dx%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8%E6%B5%81%E7%A8%8B

4.3.2. 进程内图像帧获取

首先,不考虑注入,直接在测试窗口进程中,模拟注入场景,测试DX是如何捕获图像帧。

4.3.2.1. DX8图像帧捕获

关键流程:

  • Step1:通过GetRenderTarget获取渲染目标,并通过渲染目标的GetDesc接口获取格式参数
  • Step2:利用Step1获取到的渲染目标的格式参数,使用CreateImageSurface创建一个兼容的surface
  • Step3:通过CopyRects,直接将渲染目标数据,拷贝到兼容surface
  • Step4:LockRect读取兼容surface的像素数据,增加bmp头,生成bmp图片
void OnPrePresent()
{
	static bool bFirst = true;

	if (bFirst)
	{
		bFirst = false;
		// Present之前,创建一个兼容的离屏表面
		IDirect3DSurface8* pBackBuffer = NULL;
		g_d3d8Device->GetRenderTarget(&pBackBuffer);

		D3DSURFACE_DESC desc;
		pBackBuffer->GetDesc(&desc);
		IDirect3DSurface8* pCopyBuffer = NULL;
		g_d3d8Device->CreateImageSurface(desc.Width, desc.Height, desc.Format, &pCopyBuffer);
		g_d3d8Device->CopyRects(pBackBuffer, NULL, 0, pCopyBuffer, NULL);
		
		SaveSurfaceToFile(pCopyBuffer, L"d:\\d3d8.bmp");
		pCopyBuffer->Release();
		pBackBuffer->Release();
	}
}

void SaveSurfaceToFile(IDirect3DSurface8* pCopyBuffer, LPCWSTR lpstrFileName)
{
	BITMAPFILEHEADER       bmfHdr;           //位图文件头结构
	BITMAPINFOHEADER       bi;               //位图信息头结构
	LPBITMAPINFOHEADER   lpbi;               //指向位图信息头结构

	bi.biSize = sizeof(BITMAPINFOHEADER);
	D3DSURFACE_DESC desc;
	pCopyBuffer->GetDesc(&desc);
	bi.biWidth = desc.Width;
	bi.biHeight = desc.Height;
	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 = ((desc.Width *
		bi.biBitCount + 31) / 32) * 4
		* desc.Height;

	//计算调色板大小
	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);
	}
	//   获取该调色板下新的像素值
	D3DLOCKED_RECT data;
	pCopyBuffer->LockRect(&data, NULL, 0);
	
	
	//恢复调色板      
	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);
	//   写入位图文件其余内容
	void* pData = lpbi + dwPaletteSize + sizeof(BITMAPINFOHEADER);
	memcpy(pData, data.pBits, dwBmBitsSize);
	pCopyBuffer->UnlockRect();

	WriteFile(hFile, (LPSTR)lpbi, dwDIBSize, &dwWritten, NULL);
	GlobalUnlock(hDib);
	GlobalFree(hDib);
	CloseHandle(hFile);
}

4.3.2.2. DX9图像帧捕获

关键流程:

  • Step1:通过GetRenderTarget获取渲染目标,并通过渲染目标的GetDesc接口获取格式参数
  • Step2:利用Step1获取到的渲染目标的格式参数,使用CreateOffscreenPlainSurface创建一个兼容的surface。有两种方式,一种是创建内存资源D3DPOOL_SYSTEMMEM,一种是创建显存资源D3DPOOL_DEFAULT
  • Step3:如果Step2创建的是内存资源,则使用GetRenderTargetData拷贝资源;如果是Step2创建的是显存资源,则使用StrechRect拷贝数据
  • Step4:D3DXSaveSurfaceToFile将兼容surface生成bmp图片
void OnPrePresent()
{
	static bool bFirst = true;

	if (bFirst)
	{
		bFirst = false;
		// Present之前,创建一个兼容的离屏表面
		IDirect3DSurface9* pBackBuffer = NULL;
		g_pD3D9DeviceEx->GetRenderTarget(0, &pBackBuffer);

		D3DSURFACE_DESC desc;
		pBackBuffer->GetDesc(&desc);
		IDirect3DSurface9* pCopyBuffer = NULL;
		g_pD3D9DeviceEx->CreateOffscreenPlainSurface(desc.Width, desc.Height, desc.Format, D3DPOOL_SYSTEMMEM, &pCopyBuffer, NULL);
		pCopyBuffer->UnlockRect();
		//g_pD3D9DeviceEx->StretchRect(pBackBuffer, NULL, pCopyBuffer, NULL, D3DTEXTUREFILTERTYPE::D3DTEXF_NONE);
		g_pD3D9DeviceEx->GetRenderTargetData(pBackBuffer, pCopyBuffer);
		D3DXSaveSurfaceToFileW(L"d:\\d3d9.bmp", D3DXIMAGE_FILEFORMAT::D3DXIFF_BMP, pCopyBuffer, NULL, NULL);
		pCopyBuffer->Release();
		pBackBuffer->Release();
	}
}

4.3.2.3. DX10图像帧捕获

关键流程:

  • Step1:通过SwapChain获取0号索引的后备缓存(也即最顶上渲染目标),通过GetDesc获取该后备缓存的格式参数
  • Step2:通过Step1获取到的格式参数,使用CreateTexture2D创建一个兼容的texture(注意,经测试,当samplecnt为1,无所谓创建显存还是内存资源,使用CopyResource均可复制;当samplecnt大于1,必须创建显存资源。具体原因,后续待分析)
  • Step3:调用CopyResource,直接将后备缓存数据拷贝到兼容texture
  • Step4:D3DX10SaveTextureToFile将texture存储为图片
vvoid OnPrePresent()
{
    static bool bFirst = true;

    if (bFirst)
    {
        bFirst = false;
        // 获取后备缓存
        ID3D10Resource* pBackBuffer = NULL;
        g_pSwapChain->GetBuffer(0, __uuidof(ID3D10Resource), (void**)&pBackBuffer);
        DXGI_SWAP_CHAIN_DESC swapdesc;
        g_pSwapChain->GetDesc(&swapdesc);

        // 创建一个CPU和GPU皆能访问的texture2D资源
        D3D10_TEXTURE2D_DESC desc = {};
        desc.Width = swapdesc.BufferDesc.Width;
        desc.Height = swapdesc.BufferDesc.Height;
        desc.Format = swapdesc.BufferDesc.Format;
        desc.MipLevels = 1;
        desc.ArraySize = 1;
        desc.SampleDesc.Count = 1;
        desc.Usage = D3D10_USAGE_DEFAULT;

        ID3D10Texture2D* tex = NULL;
        g_pd3dDevice->CreateTexture2D(&desc, nullptr, &tex);
        if (swapdesc.SampleDesc.Count > 1) g_pd3dDevice->ResolveSubresource(tex, 0, pBackBuffer, 0, swapdesc.BufferDesc.Format);
        else g_pd3dDevice->CopyResource(tex, pBackBuffer);

        D3DX10SaveTextureToFile(tex, D3DX10_IMAGE_FILE_FORMAT::D3DX10_IFF_BMP, L"D:\\d3d10.bmp");

        if (pBackBuffer) pBackBuffer->Release();
        if (tex) tex->Release();
    }
}

4.3.2.4. DX11图像帧捕获

关键流程,同DX10

void OnPrePresent()
{
    static bool bFirst = true;

    if (bFirst)
    {
        bFirst = false;
        // 获取后备缓存
        ID3D11Resource* pBackBuffer = NULL;
        g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Resource), (void**)&pBackBuffer);
        DXGI_SWAP_CHAIN_DESC swapdesc;
        g_pSwapChain->GetDesc(&swapdesc);

        // 创建一个CPU和GPU皆能访问的texture2D资源
        D3D11_TEXTURE2D_DESC desc = {};
        desc.Width = swapdesc.BufferDesc.Width;
        desc.Height = swapdesc.BufferDesc.Height;
        desc.Format = swapdesc.BufferDesc.Format;
        desc.MipLevels = 1;
        desc.ArraySize = 1;
        desc.SampleDesc.Count = 1;
        desc.Usage = D3D11_USAGE_DEFAULT;

        ID3D11Texture2D* tex = NULL;
        g_pd3dDevice->CreateTexture2D(&desc, nullptr, &tex);
        if (swapdesc.SampleDesc.Count > 1) g_pd3dImmediateContext->ResolveSubresource(tex, 0, pBackBuffer, 0, swapdesc.BufferDesc.Format);
        else g_pd3dImmediateContext->CopyResource(tex, pBackBuffer);

        D3DX11SaveTextureToFileW(g_pd3dImmediateContext, tex, D3DX11_IMAGE_FILE_FORMAT::D3DX11_IFF_BMP, L"D:\\d3d11.bmp");

        if (pBackBuffer) pBackBuffer->Release();
        if (tex) tex->Release();
    }
}

4.3.2.5. DX12图像帧捕获

DX12是DX11的进阶版,完全适应现代GPU架构。其关键流程,也是继承了DX11,捕获技术,基本是利用了11On12技术。

关键流程:

  • Step1:根据DX12设备接口,通过D3D11On12CreateDevice创建关联的DX11设备对象(注意,这个能力是在D3D11.dll中)
  • Step2:同时,还得通过dx11对象,拿到dx11on12设备对象,许多参数,是通过这个外挂式接口来操作的。
  • Step3:
void OnPrePresent()
{
    static bool bFirst = true;

    if (bFirst)
    {
        bFirst = false;
        DXGI_SWAP_CHAIN_DESC swapdesc;
        g_pd3dSwapChain->GetDesc(&swapdesc);

        ComPtr<ID3D11Device> pd3d11Device;
        ComPtr<ID3D11DeviceContext> pd3d11Context;
        // d3d12需要使用d3d11on12技术,通过d3d11接口获取数据
        D3D11On12CreateDevice(g_pd3dDevice.Get(), 0, NULL, 0, NULL, 0, 0,
            &pd3d11Device, &pd3d11Context, NULL);

        ComPtr<ID3D11On12Device> pd3d11On12Device;
        pd3d11Device->QueryInterface(__uuidof(ID3D11On12Device), &pd3d11On12Device);

        UINT cur_idx = g_pd3dSwapChain->GetCurrentBackBufferIndex();
        ComPtr<ID3D12Resource> backbuffer;
        g_pd3dSwapChain->GetBuffer(cur_idx, __uuidof(ID3D12Resource),
            (void**)&backbuffer);
        //backbuffer->Release();

        ComPtr<ID3D11Resource> backbuffer11;
        D3D11_RESOURCE_FLAGS rf11 = {};
        pd3d11On12Device->CreateWrappedResource(
            backbuffer.Get(), &rf11,
            D3D12_RESOURCE_STATE_COPY_SOURCE,
            D3D12_RESOURCE_STATE_PRESENT, __uuidof(ID3D11Resource),
            (void**)&backbuffer11);

        pd3d11On12Device->ReleaseWrappedResources(backbuffer11.GetAddressOf(), 1);
        pd3d11On12Device->AcquireWrappedResources(backbuffer11.GetAddressOf(), 1);

        // 创建一个CPU和GPU皆能访问的texture2D资源
        D3D11_TEXTURE2D_DESC desc = {};
        desc.Width = swapdesc.BufferDesc.Width;
        desc.Height = swapdesc.BufferDesc.Height;
        desc.Format = swapdesc.BufferDesc.Format;
        desc.MipLevels = 1;
        desc.ArraySize = 1;
        desc.SampleDesc.Count = 1;
        desc.Usage = D3D11_USAGE_STAGING;
        desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;

        ComPtr<ID3D11Texture2D> tex;
        pd3d11Device->CreateTexture2D(&desc, nullptr, &tex);
        pd3d11Context->CopyResource(tex.Get(), backbuffer11.Get());

        pd3d11On12Device->ReleaseWrappedResources(backbuffer11.GetAddressOf(), 1);
        pd3d11Context->Flush();

        ThrowIfFailed(D3DX11SaveTextureToFileW(pd3d11Context.Get(), tex.Get(), D3DX11_IMAGE_FILE_FORMAT::D3DX11_IFF_BMP, L"D:\\d3d12.bmp"));
    }
}

4.3.2.6. 示例代码

开源代码位置:https://github.com/Keenjin/D3DSample ObsCapture 分支

4.3.3. 模块注入及函数Hook

参考:https://keenjin.github.io/2019/02/WindowsHook/

5. OBS架构分解

5.1. 功能模块框图

整套系统,界面部分采用QT绘制,其余部分,大致可分为几部分:数据输入层、数据处理层、数据输出层。梳理模块及功能后,总结如下:

png

  • 数据输入层:主要对应上obs中场景和各种源,可由界面操作。
  • 数据处理层:主要是对输出数据的编码、特效处理。
  • 数据输出层:主要是对应推流、本地录制/回放录制、本地渲染预览等。

5.2. 插件式系统

obs设计上的最大目标,应该是兼容和灵活。为了兼容各种平台、各种硬件、各种软硬件环境,灵活处理各种输出类型(推流、本地录制等)、各种输入源类别、各种编码类型,obs采用了模块化插件式设计,只需要拷贝插件模块到plugins目录,即可动态加载对应的功能。

整个插件模块,主要分为这四大类:

  • 数据源(obs_source_info):图像(image_source_info)、图像幻灯片放映(slideshow_info)、场景(scene_info)、分组(group_info)、媒体源(ffmpeg)、文本(GDI+)(TextSource)、文本(FreeType 2)(freetype2_source_info_v1、freetype2_source_info_v2)、显示器捕获(monitor_capture_info、duplicator_capture_info)、游戏捕获(game_capture_info)、窗口捕获(window_capture_info)、色源(color_source_info_v1、color_source_info_v2)、视频捕获设备(decklink_source_info)、摄像头捕获(dshow_input)、音频输入捕获(wasapi_input_capture)、音频输出捕获(wasapi_output_capture)等(新版还包括网页捕获)。
  • 数据输出(obs_output_info):本地录制(ffmpeg_output)、ffmpeg_muxer、本地回放(replay_buffer)、ffmpeg_encoded_output_info、rtmp推流(rtmp_output_info)、null_output_info、flv_output_info、ftl_output_info、视频采集卡输出(decklink_output_info)
  • 编码器(obs_encoder_info):coreaudio aac编码(aac_info)、nvenc编码(nvenc_info)、opus编码(opus_encoder_info)、ffmpeg aac编码(aac_encoder_info)、ffmpeg vaapi编码(vaapi_encoder_info)、qsv编码(obs_qsv_encoder、obs_qsv_encoder_tex)、x264编码(obs_x264_encoder)、摄像头编码(dshow_c985_h264、dshow_c353_h264)
  • 推流服务(obs_service_info):rtmp_common_service、rtmp_custom_service

png

5.3. 关键流程

这里总结了一下整体关键逻辑流程,如下图

png

关键的数据流程,如下图:

png

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少