視窗/螢幕截圖適用於截圖、批註等工具場景,時時獲取視窗/螢幕影像資料流呢,下面講下視訊會議共享桌面、遠端桌面這些場景是如何實現畫面錄製的。
常見的螢幕畫面時時採集方案,主要有GDI、WGC、DXGI。
GDI
GDI(Graphics Device Interface)就是使用user32下WindowsAPI來實現,是 Windows 作業系統中最早、最基礎的圖形裝置介面,滿足所有windows平臺。螢幕/視窗截圖可以詳見: .NET 視窗/螢幕截圖 - 唐宋元明清2188 - 部落格園 (cnblogs.com)
錄製螢幕,可以基於GDI截圖方案,使用定時器捕獲螢幕資料。
GDI效能不太好,尤其是針對高幀率及高解析度需求,達到每秒20幀以上的擷取,佔用CPU就有點高了。另外GDI不能獲取滑鼠,需要在擷取的影像中把滑鼠畫上去。
所以GDI使用很方便、不依賴GPU,對效能要求不高的截圖場景建議直接使用這個方案。
WGC
Windows Graphics Capture ,是Win10引入的一種新擷取螢幕以及擷取視窗內容的機制 Screen capture - UWP applications | Microsoft Learn
WinRT提供介面訪問,Csproj屬性中新增:<UseWinRT>true</UseWinRT>
截圖程式碼實現示例:
1 public WgcCapture(IntPtr hWnd, CaptureType captureType) 2 { 3 if (!GraphicsCaptureSession.IsSupported()) 4 { 5 throw new Exception("不支Windows Graphics Capture API"); 6 } 7 var item = captureType == CaptureType.Screen ? CaptureUtils.CreateItemForMonitor(hWnd) : CaptureUtils.CreateItemForWindow(hWnd); 8 CaptureSize = new Size(item.Size.Width, item.Size.Height); 9 10 var d3dDevice = Direct3D11Utils.CreateDevice(false); 11 _device = Direct3D11Utils.CreateSharpDxDevice(d3dDevice); 12 _framePool = Direct3D11CaptureFramePool.CreateFreeThreaded(d3dDevice, pixelFormat: DirectXPixelFormat.B8G8R8A8UIntNormalized, numberOfBuffers: 1, item.Size); 13 _desktopImageTexture = CreateTexture2D(_device, item.Size); 14 _framePool.FrameArrived += OnFrameArrived; 15 item.Closed += (i, _) => 16 { 17 _framePool.FrameArrived -= OnFrameArrived; 18 StopCapture(); 19 ItemClosed?.Invoke(this, i); 20 }; 21 _session = _framePool.CreateCaptureSession(item); 22 } 23 private void OnFrameArrived(Direct3D11CaptureFramePool sender, object args) 24 { 25 try 26 { 27 using var frame = _framePool.TryGetNextFrame(); 28 if (frame == null) return; 29 var data = CopyFrameToBytes(frame); 30 var captureFrame = new CaptureFrame(CaptureSize, data); 31 FrameArrived?.Invoke(this, captureFrame); 32 } 33 catch (Exception) 34 { 35 // ignored 36 } 37 }
Windows.GraphicsCapture API負責從螢幕實際抓取畫素, GraphicsCaptureItem 類表示所捕獲的視窗或顯示, GraphicsCaptureSession 用於啟動和停止捕獲操作, Direct3D11CaptureFramePool 類維護要將螢幕內容複製到其中的幀的緩衝區。
- 建立捕捉項:使用 CreateCaptureItemForMonitor 或 CreateCaptureItemForWindow 來建立捕捉項。
- 建立D3D11裝置和上下文:呼叫 D3D11CreateDevice 建立 Direct3D 11 裝置和裝置上下文。這裡雖然沒有使用DXGI截圖,但引用了DXGI的裝置型別
- 轉換為 Direct3D 裝置:將 D3D11 裝置轉換為SharpDX Direct3D 裝置物件。
- 建立幀池和會話:使用 Direct3D11CaptureFramePool 和 GraphicsCaptureSession。
- 開始捕捉:呼叫 StartCapture 開始會話,並註冊幀到達事件。
- 處理幀:在幀到達事件中處理捕獲的幀
我們這裡是使用比較成熟的SharpDX來處理Direct3D,引用如下Nuget版本
<PackageReference Include="SharpDX" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" />
獲取到擷取的D3D物件幀,幀畫面轉資料流:
1 private byte[] CopyFrameToBytes(Direct3D11CaptureFrame frame) 2 { 3 using var bitmap = Direct3D11Utils.CreateSharpDxTexture2D(frame.Surface); 4 _device.ImmediateContext.CopyResource(bitmap, _desktopImageTexture); 5 // 將Texture2D資源對映到CPU記憶體 6 var mappedResource = _device.ImmediateContext.MapSubresource(_desktopImageTexture, 0, MapMode.Read, MapFlags.None); 7 //Bgra32 8 var bytesPerPixel = 4; 9 var width = _desktopImageTexture.Description.Width; 10 var height = _desktopImageTexture.Description.Height; 11 using var inputRgbaMat = new Mat(height, width, MatType.CV_8UC4, mappedResource.DataPointer, mappedResource.RowPitch); 12 13 var data = new byte[CaptureSize.Width * CaptureSize.Height * bytesPerPixel]; 14 if (CaptureSize.Width != width || CaptureSize.Height != height) 15 { 16 var size = new OpenCvSharp.Size(CaptureSize.Width, CaptureSize.Height); 17 Cv2.Resize(inputRgbaMat, inputRgbaMat, size, interpolation: InterpolationFlags.Linear); 18 } 19 var sourceSize = new Size(frame.ContentSize.Width, frame.ContentSize.Height); 20 if (CaptureSize == sourceSize) 21 { 22 var rowPitch = mappedResource.RowPitch; 23 for (var y = 0; y < height; y++) 24 { 25 var srcRow = inputRgbaMat.Data + y * rowPitch; 26 var destRowOffset = y * width * bytesPerPixel; 27 Marshal.Copy(srcRow, data, destRowOffset, width * bytesPerPixel); 28 } 29 } 30 else 31 { 32 Marshal.Copy(inputRgbaMat.Data, data, 0, data.Length); 33 } 34 35 _device.ImmediateContext.UnmapSubresource(_desktopImageTexture, 0); 36 return data; 37 }
將Surface物件轉換為獲取 SharpDX的Texture2D,對映到CPU以記憶體複製方式輸出影像位元組資料。
上面預設是輸出三通道8位的Bgr24,如果是四通道Bgra32可以按如下從記憶體複製:
1 using var inputRgbMat = new Mat(); 2 Cv2.CvtColor(inputRgbaMat, inputRgbMat, ColorConversionCodes.BGRA2BGR); 3 Marshal.Copy(inputRgbMat.Data, data, 0, data.Length);
拿到位元組資料,就可以儲存本地或者介面展示了 。
螢幕截圖Demo顯示:
1 private void CaptureButton_OnClick(object sender, RoutedEventArgs e) 2 { 3 var monitorHandle = MonitorUtils.GetMonitors().First().MonitorHandle; 4 var wgcCapture = new WgcCapture(monitorHandle, CaptureType.Screen); 5 wgcCapture.FrameArrived += WgcCapture_FrameArrived; 6 wgcCapture.StartCapture(); 7 } 8 9 private void WgcCapture_FrameArrived(object? sender, CaptureFrame e) 10 { 11 Application.Current.Dispatcher.Invoke(() => 12 { 13 var stride = e.Size.Width * 4; // 4 bytes per pixel in BGRA format 14 var bitmap = BitmapSource.Create(e.Size.Width, e.Size.Height, 96, 96, PixelFormats.Bgra32, null, e.Data, stride); 15 bitmap.Freeze(); 16 CaptureImage.Source = bitmap; 17 }); 18 }
WGC利用了現代圖形硬體和作業系統特性、能夠提供高效能和低延遲的螢幕捕抓,適用於實時性比較高的場景如螢幕錄製、視訊會議等應用。
更多的,可以參考官網螢幕捕獲到影片 - UWP applications | Microsoft Learn。也可以瀏覽、執行我的Demo:kybs00/CaptureImageDemo (github.com)
DXGI
全名DirectX Graphics Infrastructure。從Win8開始,微軟引入了一套新的介面Desktop Duplication API,而由於Desktop Duplication API是透過DXGI來提供桌面影像的,速度非常快。
DXGI使用GPU,所以cpu佔用率很低,效能很高。DXGI官網文件:DXGI - Win32 apps | Microsoft Learn
因為DXGI也是使用DirectX,所以很多介面與WGC差不多。也就是透過D3D,各種QueryInterface,各種Enum,核心方法是AcquireNextFrame
它有個缺點,沒辦法捕獲視窗內容。所以視訊會議共享視窗,是無法透過DXGI實現
我們看看Demo呼叫程式碼,
1 private void CaptureButton_OnClick(object sender, RoutedEventArgs e) 2 { 3 var monitorDxgiCapture = new MonitorDxgiCapture(); 4 monitorDxgiCapture.FrameArrived += WgcCapture_FrameArrived; 5 monitorDxgiCapture.StartCapture(); 6 } 7 8 private void WgcCapture_FrameArrived(object? sender, CaptureFrame e) 9 { 10 Application.Current?.Dispatcher.Invoke(() => 11 { 12 var stride = e.Size.Width * 4; // 4 bytes per pixel in BGRA format 13 var bitmap = BitmapSource.Create(e.Size.Width, e.Size.Height, 96, 96, PixelFormats.Bgra32, null, e.Data, stride); 14 15 bitmap.Freeze(); 16 CaptureImage.Source = bitmap; 17 }); 18 }
捕獲畫面幀資料:
1 [HandleProcessCorruptedStateExceptions] 2 private CaptureFrame CaptureFrame() 3 { 4 try 5 { 6 var data = new byte[CaptureSize.Width * CaptureSize.Height * 4]; 7 var result = _mDeskDupl.TryAcquireNextFrame(TimeOut, out _, out var desktopResource); 8 if (result.Failure) return null; 9 10 using var tempTexture = desktopResource?.QueryInterface<Texture2D>(); 11 _mDevice.ImmediateContext.CopyResource(tempTexture, _desktopImageTexture); //複製影像紋理:GPU硬體加速的紋理複製 12 desktopResource?.Dispose(); 13 14 var desktopSource = _mDevice.ImmediateContext.MapSubresource(_desktopImageTexture, 0, MapMode.Read, MapFlags.None); 15 using var inputRgbaMat = new Mat(_screenSize.Height, _screenSize.Width, MatType.CV_8UC4, desktopSource.DataPointer); 16 if (CaptureSize.Width != _screenSize.Width || CaptureSize.Height != _screenSize.Height) 17 { 18 var size = new OpenCvSharp.Size(CaptureSize.Width, CaptureSize.Height); 19 Cv2.Resize(inputRgbaMat, inputRgbaMat, size, interpolation: InterpolationFlags.Linear); 20 } 21 Marshal.Copy(inputRgbaMat.Data, data, 0, data.Length); 22 23 var captureFrame = new CaptureFrame(CaptureSize, data); 24 _mDevice.ImmediateContext.UnmapSubresource(_desktopImageTexture, 0); 25 //釋放幀 26 _mDeskDupl.ReleaseFrame(); 27 return captureFrame; 28 } 29 catch (AccessViolationException) 30 { 31 return null; 32 } 33 catch (Exception) 34 { 35 return null; 36 } 37 }
也是使用硬體加速將2D紋理資源複製,然後透過記憶體複製輸出為位元組資料。
1080P的本地錄屏、顯示,CPU、GPU使用情況如下:
1080P和WGC方案沒有明顯差別,延時也接近。但4K、8K解析度下,DXGI方案更優,能夠直接管理圖形硬體和提供高效能渲染。它是與核心模式驅動程式和系統硬體進行通訊的,借用下官網的架構圖:
所以在需要極低延遲和高幀率的4K場景中,DXGI能提供必要的效能最佳化。
上面3個方案Demo示例,詳細程式碼都在github倉庫:kybs00/CaptureImageDemo (github.com)
總結下這三個方案,
GDI:適用於所有 Windows 版本,但效能較低。
WGC:Win10 1803版本以上,高效能和低延遲,螢幕及視窗均支援。
DXGI:Win8版本以上,適用於高解析度高幀率等高效能的需求,並且只支援螢幕錄製、不支援視窗。
錄製主要是錄屏、直播、遠端桌面、視訊會議、傳屏等場景。錄製螢幕/視窗建議優先使用WGC,然後用DXGI相容win8;如果僅錄製螢幕且高解析度、高幀率場景,建議優先DXGI
關鍵字:錄屏、錄製視窗、高效能螢幕捕獲