.NET 視窗/螢幕錄製

唐宋元明清2188發表於2024-08-09

視窗/螢幕截圖適用於截圖、批註等工具場景,時時獲取視窗/螢幕影像資料流呢,下面講下視訊會議共享桌面、遠端桌面這些場景是如何實現畫面錄製的。

常見的螢幕畫面時時採集方案,主要有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 類維護要將螢幕內容複製到其中的幀的緩衝區。

WGC截圖流程:
  1. 建立捕捉項:使用 CreateCaptureItemForMonitor 或 CreateCaptureItemForWindow 來建立捕捉項。
  2. 建立D3D11裝置和上下文:呼叫 D3D11CreateDevice 建立 Direct3D 11 裝置和裝置上下文。這裡雖然沒有使用DXGI截圖,但引用了DXGI的裝置型別
  3. 轉換為 Direct3D 裝置:將 D3D11 裝置轉換為SharpDX Direct3D 裝置物件。
  4. 建立幀池和會話:使用 Direct3D11CaptureFramePool 和 GraphicsCaptureSession。
  5. 開始捕捉:呼叫 StartCapture 開始會話,並註冊幀到達事件。
  6. 處理幀:在幀到達事件中處理捕獲的幀

我們這裡是使用比較成熟的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紋理路徑不一樣,但其它的程式碼差不多。也是引用Nuget包SharpDX(但不需要WRT了),使用硬體加速將2D紋理資源複製,然後透過記憶體複製輸出為位元組資料。

1080P的本地錄屏、顯示,CPU、GPU使用情況如下:

1080P,DXGI和WGC方案沒有明顯差別,延時也接近。

再看看4K下,WGC、DXGI在本地上錄製、顯示的延遲、CPUGPU資源消耗情況:

DXGI資源主要是GPU消耗略多。延遲方面WGC200ms、DXGI100ms,資料會有波動,大概這個數吧,截圖我們僅做參考。

所以4K、8K解析度下,DXGI方案更優,能夠直接管理圖形硬體和提供高效能渲染。它是與核心模式驅動程式和系統硬體進行通訊的,借用下官網的架構圖:

所以在需要極低延遲和高幀率的4K場景中,DXGI能提供必要的效能最佳化。

上面3個方案Demo示例,詳細程式碼都在github倉庫:kybs00/CaptureImageDemo (github.com)

總結下這三個方案

GDI:適用於所有 Windows 版本,但效能較低。

WGC:Win10 1803版本以上,高效能和低延遲,螢幕及視窗均支援。

DXGI:Win8版本以上,適用於高解析度高幀率等高效能的需求,並且只支援螢幕錄製、不支援視窗。

錄製主要是本地錄屏、直播、遠端桌面、視訊會議、傳屏等場景。錄製螢幕/視窗建議優先使用WGC,然後用DXGI相容win8;如果僅錄製螢幕且高解析度、高幀率場景,建議優先DXGI

關鍵字:錄屏、錄製視窗、高效能螢幕捕獲

相關文章