共计 3043 个字符,预计需要花费 8 分钟才能阅读完成。
当今,视频直播技术和实时音视频技术已经是很多行业必备,典型的应用场景有教育直播、远程视频会议、互联网娱乐等。在移动端发起直播,其画面源的种类是十分有限的,无非是取摄像头、截屏等。PC 端由于其系统资源充足,应用程序丰富,画面源种类多样,更适合作为主播程序运行的平台。在实际应用中,经常有一些场景是需要将不同的画面源合在一起,然后推流出去的。本文粗浅介绍一些网易云信在开发过程中总结的一些获取不同画面源的画面并将其合并的方法。相关阅读推荐《如何快速实现移动端短视频功能?》
《视频私有云实战:基于 Docker 构建点播私有云平台》
各类画面源的截取
摄像头画面
Windows 下采集摄像头画面,DShow 是最常用的方法之一。通过 DShow 采集摄像头数据,创建视频采集 Filter,将其加入到图表 IGraphBuilder 中,用 IMediaControl 接口来控制流媒体在 Filter Graph 中的流动,再通过 Render 来获取视频的原始数据。以上流程封装在了我们的 SDK 中,用户可以直接调用 SDK 接口。
桌面取屏及应用程序窗口截取
在 Windows 系统中,桌面和所有应用程序窗口一样,本身也是一个 HWND 窗口,因此可以放在一起讨论。获取一个窗口的位图数据,最常用的方法是:创建一个用来接收窗口画面的 HBITMAP 位图对象以及一个 HDC 设备上下文对象,用 SelectObject 将两者绑定,然后用 BitBlt 从被截取窗口的 HDC 将数据拷贝到目标 HDC。下面列出关键代码:
其他截屏 / 截窗口方法
教育直播中,PPT 分享是非常重要的一个场景。但是据我考查,自从 Microsoft Office 2013 之后,BitBlt 就取不到 Word、Excel、PPT 窗口的内容了,截到的是一片白色。但是用 PrintWindow 这个 Windows API 却可以取到。调用 PrintWindow 的程序会收到 WM_PRINT 或 WM_PRINTCLIENT 消息。PrintWindow 的效率比 BitBlt 低,但当 BitBlt 无法取到时,可以用 PrintWindow。
越来越多的程序的画面是在显存中的,此时,BitBlt 和 PrintWindow 都不管用(得到的都是一块黑色的位图)。可以考虑用 DirectX 的方法。而且 DirectX 方法由于使用了 GPU,所以相较前面两种方法效率更高。以下是 DirectX 截屏的代码:
获取本地图片的位图数据
将本地图片(jpg、bmp、png、gif 等格式)加载到内存,并取得其位图句柄或像素首地址的方法有很多种。这里列举几种最常见的。
GdiPlus 方法比较简单。首先是通过图片路径创建一个 Gdiplus::Bitmap 对象,通过 Gdiplus::Bitmap::LockBits() 方法可以得到图片的数据,存放在一个 Gdiplus::BitmapData 结构中。Gdiplus::BitmapData::Scan0 就是图片像素数据的首地址。如果想得到该图片的 HBITMAP 句柄,只需调 Gdiplus::Bitmap::GetHBITMAP() 即可。
另一种方法是使用 Windows API LoadImage 来加载一个本地 bmp 图片得到 HBITMAP 句柄,但这种方法似乎只能加载位图文件 (.bmp 格式)。使用 ATL 的 CImage 只需要 3 行代码即可得到一个图片文件的 HBITMAP 句柄。
画面合成主播常常希望同时将自己的摄像头画面和桌面内容或者某个程序的画面共享给观众,有时甚至需要同一时刻分享 10 个以上的画面源。这时候,需要将多个画面粘贴到一个目标画面上,我们称这个过程为画面合成。合成的画面通常还要支持改变各个画面的尺寸、位置等操作。这样一来,程序性能成了瓶颈问题。
首先,对于各种画面源的截取应该尽量采用高效的方式,其次,画面的拉伸压缩是比较耗性能的地方。在 1 秒钟需要合成 20 帧画面的要求下,应该避免直接强行压缩 HBITMAP,而是采用一些有加速的方案。
LibYuv 方案
我们找到一个一个 yuv 库(LibYuv Project),支持图形数据从 rgb 格式到各种 yuv 格式之间的互相转换(定义在 libyuv/convert.h 中)。比较重要的一点是,它对 yuv 格式图形的拉伸和压缩以及其他各种变换(定义在 libyuv/scale.h 中)是有加速的。正好我们最终要推流的格式也是 yuv 格式的,所以我们方案的流程是:取得各个画面源的画面之后,先将它们各自转化为 yuv 格式,然后把这些 yuv 画面按照我们制定的方式粘贴到一个目标 yuv 画面上,最后将目标 yuv 画面数据推流出去。另外,由于主播的窗口上也要显示合并画面,所以还要把目标画面转成 rgb 格式渲染到窗口 HDC 上。
当然,由于存在 rgb 格式和 yuv 格式之间反复的转换以及频繁的 scale,而且 yuv 加速毕竟是软件方式,程序的 CPU 占用率还是有点高。如果能采用 DirectX、OpenGL 等硬件加速解决方案,对程序性能以及用户体验的提升应该是比较明显的。
DirectX 9 方案
在 DirectX 9 方案中,我们的每个画面源以及最终的目标合成画面,都对应一个表面(IDirect3DSurface9)和一个纹理(IDirect3DTexture9)。
由于画面源的颜色内存可能会被频繁访问和修改,所以创建其表面或纹理时,应该将其创建在系统内存或 AGP 中(D3DPOOL_MANAGED)而不是显存中。对于 yuv 格式的摄像头数据或网络视频帧,DirectX 可以创建能直接接受 yuv 数据的纹理(D3DFMT_UYVY)。合成的时候,调用 IDirect3DDevice9::DrawPrimitive() 来将每个画面源绘制到目标画面上。
而最终合成画面是要显示到窗口上的,所以应该创建在显存中(D3DPOOL_DEFAULT)。渲染的时候,调用 IDirect3DDevice9::DrawPrimitive() 将目标画面的纹理绘制到窗口的渲染目标纹理上,或者调用 IDirect3DDevice9::StretchRect() 将目标画面的表面粘贴到窗口的 back buffer 上。
另外,由于要取得目标画面的数据用于推流,我们还要调用 IDirect3DDevice9::CreateOffscreenPlainSurface() 在系统内存中(D3DPOOL_SYSTEMMEM)创建一个离屏表面,用 IDirect3DDevice9::GetRenderTargetData() 将目标画面取到离屏表面上,然后 IDirect3DSurface9::LockRect() 就能得到目标画面的 rgb 格式数据了,将其转化为 yuv 格式就可以推流出去了。
总 结直播产品由于需要对每一帧画面做处理,画面的清晰度要高,帧率还不能太低,所以通常会存在消耗系统资源过多的问题。无论是取画面还是合成画面,方法有很多,不仅限于上面几种。Win API 效率一般,如果对程序性能要求高,就要在其他方面去想法设法减少资源消耗。而 DirectX 虽然对 2D 图形加速不如 3D 加速那么显著,但还是胜过 Win API 的。需要注意的是,使用 DirectX 时要非常清楚各个参数的意义,比如设备类型(D3DDEVTYPE)、内存池类型(D3DPOOL)、用途类型(D3DUSAGE)等等。参数用错,可能导致其性能还不如 Win API。
以上就是视频直播中 Windows 中各类画面源的截取和合成方法总结。
另外,想要获取更多产品干货、技术干货,记得关注网易云信博客。