• ScreenCapture:通过DirectX 库进行屏幕捕获


    具有音频混合功能的 DirectX 硬件屏幕捕获和编码。 H264/H265/VP80/VP90/FLAC/MP3。

    以硬件方式捕获视频和屏幕截图。

    介绍

    有很多关于它的东西。 这是一个简单的单头文件,硬件加速。 如果使用 Windows 8 或更高版本,您可以轻松地将其包含在您的项目中。

    要求

    Windows 8 或更高版本。

    视频截取

    我们需要在 DXGI 的帮助下枚举我们的适配器和监视器的数量:

    1. static void GetAdapters(std::vector>& a)
    2. {
    3. CComPtr df;
    4. CreateDXGIFactory1(__uuidof(IDXGIFactory1),(void**)&df);
    5. a.clear();
    6. if (!df)
    7. return;
    8. int L = 0;
    9. for (;;)
    10. {
    11. CComPtr lDxgiAdapter;
    12. df->EnumAdapters1(L, &lDxgiAdapter);
    13. if (!lDxgiAdapter)
    14. break;
    15. L++;
    16. a.push_back(lDxgiAdapter);
    17. }
    18. }

    然后,我们将使用其中一个或默认值来实例化 DirectX 11 设备:

    1. HRESULT CreateDirect3DDevice(IDXGIAdapter1* g)
    2. {
    3. HRESULT hr = S_OK;
    4. // 支持的驱动程序类型
    5. D3D_DRIVER_TYPE DriverTypes[] =
    6. {
    7. D3D_DRIVER_TYPE_HARDWARE,
    8. D3D_DRIVER_TYPE_WARP,
    9. D3D_DRIVER_TYPE_REFERENCE,
    10. };
    11. UINT NumDriverTypes = ARRAYSIZE(DriverTypes);
    12. // 支持的功能级别
    13. D3D_FEATURE_LEVEL FeatureLevels[] =
    14. {
    15. D3D_FEATURE_LEVEL_11_0,
    16. D3D_FEATURE_LEVEL_10_1,
    17. D3D_FEATURE_LEVEL_10_0,
    18. D3D_FEATURE_LEVEL_9_3,
    19. D3D_FEATURE_LEVEL_9_2,
    20. D3D_FEATURE_LEVEL_9_1
    21. };
    22. UINT NumFeatureLevels = ARRAYSIZE(FeatureLevels);
    23. D3D_FEATURE_LEVEL FeatureLevel;
    24. // 创建设备
    25. for (UINT DriverTypeIndex = 0; DriverTypeIndex < NumDriverTypes; ++DriverTypeIndex)
    26. {
    27. hr = D3D11CreateDevice(g, DriverTypes[DriverTypeIndex],
    28. nullptr, D3D11_CREATE_DEVICE_VIDEO_SUPPORT, FeatureLevels, NumFeatureLevels,
    29. D3D11_SDK_VERSION, &device, &FeatureLevel, &context);
    30. if (SUCCEEDED(hr))
    31. {
    32. // 设备创建成功,无需循环
    33. break;
    34. }
    35. }
    36. if (FAILED(hr))
    37. return hr;
    38. return S_OK;
    39. }

    我们要创建输出的桌面副本,然后:

    1. bool Prepare(UINT Output = 0)
    2. {
    3. // 获取 DXGI 设备
    4. CComPtr lDxgiDevice;
    5. lDxgiDevice = device;
    6. if (!lDxgiDevice)
    7. return 0;
    8. // 获取 DXGI 适配器
    9. CComPtr lDxgiAdapter;
    10. auto hr = lDxgiDevice->GetParent(
    11. __uuidof(IDXGIAdapter),
    12. reinterpret_cast<void**>(&lDxgiAdapter));
    13. if (FAILED(hr))
    14. return 0;
    15. lDxgiDevice = 0;
    16. // 获取输出
    17. CComPtr lDxgiOutput;
    18. hr = lDxgiAdapter->EnumOutputs(Output, &lDxgiOutput);
    19. if (FAILED(hr))
    20. return 0;
    21. lDxgiAdapter = 0;
    22. DXGI_OUTPUT_DESC lOutputDesc;
    23. hr = lDxgiOutput->GetDesc(&lOutputDesc);
    24. // QI for Output 1
    25. CComPtr lDxgiOutput1;
    26. lDxgiOutput1 = lDxgiOutput;
    27. if (!lDxgiOutput1)
    28. return 0;
    29. lDxgiOutput = 0;
    30. // 创建桌面副本
    31. hr = lDxgiOutput1->DuplicateOutput(
    32. device,
    33. &lDeskDupl);
    34. if (FAILED(hr))
    35. return 0;
    36. lDxgiOutput1 = 0;
    37. // 创建 GUI 绘图纹理
    38. lDeskDupl->GetDesc(&lOutputDuplDesc);
    39. D3D11_TEXTURE2D_DESC desc = {};
    40. desc.Width = lOutputDuplDesc.ModeDesc.Width;
    41. desc.Height = lOutputDuplDesc.ModeDesc.Height;
    42. desc.Format = lOutputDuplDesc.ModeDesc.Format;
    43. desc.ArraySize = 1;
    44. desc.BindFlags = D3D11_BIND_FLAG::D3D11_BIND_RENDER_TARGET;
    45. desc.MiscFlags = D3D11_RESOURCE_MISC_GDI_COMPATIBLE;
    46. desc.SampleDesc.Count = 1;
    47. desc.SampleDesc.Quality = 0;
    48. desc.MipLevels = 1;
    49. desc.CPUAccessFlags = 0;
    50. desc.Usage = D3D11_USAGE_DEFAULT;
    51. hr = device->CreateTexture2D(&desc, NULL, &lGDIImage);
    52. if (FAILED(hr))
    53. return 0;
    54. if (lGDIImage == nullptr)
    55. return 0;
    56. // 创建 CPU 访问纹理
    57. desc.Width = lOutputDuplDesc.ModeDesc.Width;
    58. desc.Height = lOutputDuplDesc.ModeDesc.Height;
    59. desc.Format = lOutputDuplDesc.ModeDesc.Format;
    60. desc.ArraySize = 1;
    61. desc.BindFlags = 0;
    62. desc.MiscFlags = 0;
    63. desc.SampleDesc.Count = 1;
    64. desc.SampleDesc.Quality = 0;
    65. desc.MipLevels = 1;
    66. desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE;
    67. desc.Usage = D3D11_USAGE_STAGING;
    68. hr = device->CreateTexture2D(&desc, NULL, &lDestImage);
    69. if (FAILED(hr))
    70. return 0;
    71. if (lDestImage == nullptr)
    72. return 0;
    73. return 1;
    74. }

    要获取屏幕截图,我们循环:

    1. hr = cap.lDeskDupl->AcquireNextFrame(
    2. 0,
    3. &lFrameInfo,
    4. &lDesktopResource);
    5. if (hr == DXGI_ERROR_WAIT_TIMEOUT)
    6. hr = S_OK;
    7. if (FAILED(hr))
    8. break;
    9. if (lDesktopResource && !cap.Get(lDesktopResource, dp.Cursor,
    10. dp.rx.right && dp.rx.bottom ? &dp.rx : 0))
    11. break;

    get() 方法将返回位图,可选地包含和裁剪的光标:

    1. bool Get(IDXGIResource* lDesktopResource,bool Curs,RECT* rcx = 0)
    2. {
    3. // ID3D11Texture2D 的 QI
    4. CComPtr lAcquiredDesktopImage;
    5. if (!lDesktopResource)
    6. return 0;
    7. auto hr = lDesktopResource->QueryInterface(IID_PPV_ARGS(&lAcquiredDesktopImage));
    8. if (!lAcquiredDesktopImage)
    9. return 0;
    10. lDesktopResource = 0;
    11. // 将图像复制到 GDI 绘图纹理中
    12. context->CopyResource(lGDIImage, lAcquiredDesktopImage);
    13. // 将光标图像绘制到 GDI 绘图纹理中
    14. CComPtr lIDXGISurface1;
    15. lIDXGISurface1 = lGDIImage;
    16. if (!lIDXGISurface1)
    17. return 0;
    18. CURSORINFO lCursorInfo = { 0 };
    19. lCursorInfo.cbSize = sizeof(lCursorInfo);
    20. auto lBoolres = GetCursorInfo(&lCursorInfo);
    21. if (lBoolres == TRUE)
    22. {
    23. if (lCursorInfo.flags == CURSOR_SHOWING && Curs)
    24. {
    25. auto lCursorPosition = lCursorInfo.ptScreenPos;
    26. // auto lCursorSize = lCursorInfo.cbSize;
    27. HDC lHDC;
    28. lIDXGISurface1->GetDC(FALSE, &lHDC);
    29. DrawIconEx(
    30. lHDC,
    31. lCursorPosition.x,
    32. lCursorPosition.y,
    33. lCursorInfo.hCursor,
    34. 0,
    35. 0,
    36. 0,
    37. 0,
    38. DI_NORMAL | DI_DEFAULTSIZE);
    39. lIDXGISurface1->ReleaseDC(nullptr);
    40. }
    41. }
    42. // 将图像复制到 CPU 访问纹理中
    43. context->CopyResource(lDestImage, lGDIImage);
    44. // Copy from CPU access texture to bitmap buffer
    45. D3D11_MAPPED_SUBRESOURCE resource;
    46. UINT subresource = D3D11CalcSubresource(0, 0, 0);
    47. hr = context->Map(lDestImage, subresource, D3D11_MAP_READ_WRITE, 0, &resource);
    48. if (FAILED(hr))
    49. return 0;
    50. auto sz = lOutputDuplDesc.ModeDesc.Width
    51. * lOutputDuplDesc.ModeDesc.Height * 4;
    52. auto sz2 = sz;
    53. buf.resize(sz);
    54. if (rcx)
    55. {
    56. sz2 = (rcx->right - rcx->left) * (rcx->bottom - rcx->top) * 4;
    57. buf.resize(sz2);
    58. sz = sz2;
    59. }
    60. UINT lBmpRowPitch = lOutputDuplDesc.ModeDesc.Width * 4;
    61. if (rcx)
    62. lBmpRowPitch = (rcx->right - rcx->left) * 4;
    63. UINT lRowPitch = std::min(lBmpRowPitch, resource.RowPitch);
    64. BYTE* sptr = reinterpret_cast(resource.pData);
    65. BYTE* dptr = buf.data() + sz - lBmpRowPitch;
    66. if (rcx)
    67. sptr += rcx->left * 4;
    68. for (size_t h = 0; h < lOutputDuplDesc.ModeDesc.Height; ++h)
    69. {
    70. if (rcx && h < (size_t)rcx->top)
    71. {
    72. sptr += resource.RowPitch;
    73. continue;
    74. }
    75. if (rcx && h >= (size_t)rcx->bottom)
    76. break;
    77. memcpy_s(dptr, lBmpRowPitch, sptr, lRowPitch);
    78. sptr += resource.RowPitch;
    79. dptr -= lBmpRowPitch;
    80. }
    81. context->Unmap(lDestImage, subresource);
    82. return 1;
    83. }

    之后,您可以将“buf”数据输入媒体基金会的接收器写入器。

    音频捕捉

    您将使用 IAudioClient 获取 IAudioCaptureClient 以在单独的线程中录制音频。

    1. void ThreadLoopCapture()
    2. {
    3. UINT64 up, uq;
    4. while (Capturing)
    5. {
    6. if (hEv)
    7. WaitForSingleObject(hEv, INFINITE);
    8. if (!Capturing)
    9. break;
    10. auto hr = cap->GetBuffer(&pData, &framesAvailable, &flags, &up, &uq);
    11. if (FAILED(hr))
    12. break;
    13. if (framesAvailable == 0)
    14. continue;
    15. auto ThisAudioBytes = framesAvailable * wfx.Format.nChannels *
    16. wfx.Format.wBitsPerSample/8 ;
    17. AudioDataX->PushX((const char*)pData, ThisAudioBytes);
    18. cap->ReleaseBuffer(framesAvailable);
    19. }
    20. CapturingFin1 = true;
    21. }

    如果录音设备是通过 loopback 的播放设备,则必须确保播放了某些内容,否则 Core Audio API 什么也不记录。 所以我们必须玩沉默:

    1. void PlaySilence(REFERENCE_TIME rt)
    2. {
    3. // ns
    4. rt /= 10000;
    5. // in SR , 1000 ms
    6. // ? , rt ms
    7. auto ns = (wfx.Format.nSamplesPerSec * rt);
    8. ns /= 1000;
    9. while (Capturing)
    10. {
    11. if (!ren)
    12. break;
    13. Sleep((DWORD)(rt / 2));
    14. if (!Capturing)
    15. break;
    16. // 查看有多少可用的缓冲区空间。
    17. UINT32 numFramesPadding = 0;
    18. auto hr = ac2->GetCurrentPadding(&numFramesPadding);
    19. if (FAILED(hr))
    20. break;
    21. auto numFramesAvailable = ns - numFramesPadding;
    22. if (!numFramesAvailable)
    23. continue;
    24. BYTE* db = 0;
    25. hr = ren->GetBuffer((UINT32)numFramesAvailable, &db);
    26. if (FAILED(hr))
    27. break;
    28. auto bs = numFramesAvailable * wfx.Format.nChannels * wfx.Format.wBitsPerSample / 8;
    29. memset(db, 0,(size_t) bs);
    30. ren->ReleaseBuffer((UINT32)numFramesAvailable, 0); //AUDCLNT_BUFFERFLAGS_SILENT
    31. }
    32. CapturingFin2 = true;
    33. }

    当有许多音频流时,您必须将它们混合在一个缓冲区中。 这是使用我自己的 REBUFFER 和 MIXBUFFER 完成的:

    1. struct REBUFFER
    2. {
    3. std::recursive_mutex m;
    4. std::vector<char> d;
    5. AHANDLE Has = CreateEvent(0, TRUE, 0, 0);
    6. MIXBUFFER<float> mb;
    7. void FinMix(size_t sz, float* A = 0)
    8. {
    9. mb.Fin(sz / sizeof(float), A);
    10. }
    11. size_t PushX(const char* dd, size_t sz, float* A = 0, float V = 1.0f)
    12. {
    13. REBUFFERLOCK l(m);
    14. auto s = d.size();
    15. d.resize(s + sz);
    16. if (dd)
    17. memcpy(d.data() + s, dd, sz);
    18. else
    19. memset(d.data() + s, 0, sz);
    20. char* a1 = d.data();
    21. a1 += s;
    22. mb.Set((float*)a1);
    23. mb.count = 1;
    24. SetEvent(Has);
    25. float* b = (float*)(d.data() + s);
    26. if (V > 1.01f || V < 0.99f)
    27. {
    28. auto st = sz / sizeof(float);
    29. for (size_t i = 0; i < st; i++)
    30. b[i] *= V;
    31. }
    32. if (A)
    33. {
    34. *A = Peak<float>(b, sz / sizeof(float));
    35. }
    36. return s + sz;
    37. }
    38. size_t Av()
    39. {
    40. REBUFFERLOCK l(m);
    41. return d.size();
    42. }
    43. size_t PopX(char* trg, size_t sz, DWORD wi = 0, bool NR = false)
    44. {
    45. if (wi)
    46. WaitForSingleObject(Has, wi);
    47. REBUFFERLOCK l(m);
    48. if (sz >= d.size())
    49. sz = d.size();
    50. if (sz == 0)
    51. return 0;
    52. if (trg)
    53. memcpy(trg, d.data(), sz);
    54. if (NR == false)
    55. d.erase(d.begin(), d.begin() + sz);
    56. if (d.size() == 0)
    57. ResetEvent(Has);
    58. return sz;
    59. }
    60. void Clear()
    61. {
    62. REBUFFERLOCK l(m);
    63. d.clear();
    64. }
    65. };

    如果您有音频,则视频会与之同步。

    使用库

    1. #include "stdafx.h"
    2. #include "capture.hpp"
    3. #include
    4. int wmain()
    5. {
    6. CoInitializeEx(0, COINIT_APARTMENTTHREADED);
    7. MFStartup(MF_VERSION);
    8. std::cout << "Capturing screen for 10 seconds...";
    9. DESKTOPCAPTUREPARAMS dp;
    10. dp.f = L"capture.mp4";
    11. dp.EndMS = 10000;
    12. DesktopCapture(dp);
    13. std::cout << "Done.\r\n";
    14. return 0;
    15. }

    DESKTOPCAPTUREPARAMS 的定义如下:

    1. struct DESKTOPCAPTUREPARAMS
    2. {
    3. bool HasVideo = 1;
    4. bool HasAudio = 1;
    5. std::vectorint>>> AudioFrom;
    6. GUID VIDEO_ENCODING_FORMAT = MFVideoFormat_H264;
    7. GUID AUDIO_ENCODING_FORMAT = MFAudioFormat_MP3;
    8. std::wstring f;
    9. void* cb = 0;
    10. std::functionconst BYTE* d, size_t sz,void* cb)> Streamer;
    11. std::functionconst BYTE* d, size_t sz,void* cb)> Framer;
    12. std::function<void(IMFAttributes* a)> PrepareAttributes;
    13. int fps = 25;
    14. int NumThreads = 0;
    15. int Qu = -1;
    16. int vbrm = 0;
    17. int vbrq = 0;
    18. int BR = 4000;
    19. int NCH = 2;
    20. int SR = 44100;
    21. int ABR = 192;
    22. bool Cursor = true;
    23. RECT rx = { 0,0,0,0 };
    24. HWND hWnd = 0;
    25. IDXGIAdapter1* ad = 0;
    26. UINT nOutput = 0;
    27. unsigned long long StartMS = 0; // 0, none
    28. unsigned long long EndMS = 0; // 0, none
    29. bool MustEnd = false;
    30. bool Pause = false;
    31. };

    说明:

    HasVideo = 1 -> 您正在捕捉视频。如果设置了此项,则无论您是否有音频,输出文件都必须是 MP4 或 ASF。
    HasAudio = 1 -> 您正在捕获音频。如果已设置并且您没有视频,则输出文件必须是 MP3 或 FLAC。
    AudioFrom = 要捕获的音频设备的向量。每个元素都是设备唯一 ID 的元组(由枚举返回,请参阅 VISTAMIXERS::EnumVistaMixers())和您要记录的通道的向量。
    该库还可以在环回中从播放设备(例如您的扬声器)进行录制。您可以指定多个录制源,库会将它们全部混合到最终的音频流中。

    VIDEO_ENCODING_FORMAT -> MFVideoFormat_H264、MFVideoFormat_HEVC、MFVideoFormat_VP90、MFVideoFormat_VP80 之一。
    AUDIO_ENCODING_FORMAT -> MFAudioFormat_MP3 或 MFAudioFormat_FLAC 或 MFAudioFormat_AAC 之一。 MP3 和 AAC 仅支持 44100/48000 2 通道输出。
    f -> 目标文件名(MP3/FLAC 仅用于音频,MP4/ASF 其他)
    fps -> 每秒帧数
    NumThreads -> 视频编码器的线程,默认为 0。可以是 0-16。
    Qu -> 如果 >= 0 且 <= 0,质量与速度的视频因素
    vbrm 和 vbrq -> 如果为 2,则 vbrq 是介于 0 和 100 之间的质量值(BR 被忽略)
    BR -> 以 KBps 为单位的视频比特率,默认 4000。如果 vbrm 为 2,则忽略 BR
    NCH -> 音频输出通道
    SR -> 音频输出采样率
    ABR -> MP3 的音频比特率(Kbps)
    Cursor -> true 捕获光标
    rx -> 如果不是 {0},则仅捕获此特定矩形
    hWnd -> 如果不是 {0},则仅捕获此 HWND。如果 HWND 为 0 且 rx = {0},则捕获整个屏幕
    ad -> 如果不是 0,则指定如果您有超过 1 个适配器,您要捕获哪个适配器
    nOutput -> 要捕获的监视器的索引。 0 是第一个监视器。对于多个监视器,这指定了监视器。
    EndMS -> 如果不为 0,则库在 EndMs 毫秒被捕获时停止。否则,您必须通过将“MustEnd”设置为 true 来停止库。
    MustEnd -> 设置为 true 以使库停止捕获
    暂停 -> 如果为真,则暂停捕获
    如果要捕获到缓冲区,则必须将“f”参数留空并使用 Streamer 参数。只要您返回 S_OK,这就会调用您的回调。如果您使用 ASF 容器,则无需执行任何操作。如果您想使用 MP4 流,则必须准备流示例描述(请参阅此帖子)。您可以使用它通过 HTTP 流式传输您的桌面。

    捕获帧

    您可以使用“Framer”回调,而不是捕获压缩视频。 只要您返回 S_FALSE,这将返回您请求的分辨率的原始 RGBA 倒置数组。 一旦你返回 S_OK,函数就会返回。

  • 相关阅读:
    Qt——对话框简介
    从“Hello,World”谈起(C++入门)
    2022-09-12 mysql语法分析实现
    2022年中总结关键词:裁员、年终奖、晋升、涨薪、疫情
    Visual Studio快捷键记录
    网安朝阳·西门子白帽黑客大赛 | 聚焦实战攻防竞赛 促进网安人才发展
    k8s 中的 ingress 使用细节
    Springboot+网上投资借贷中介服务 毕业设计-附源码221506
    如何在uni-app小程序端实现长按复制功能
    【Python学习笔记】Python中的heapq
  • 原文地址:https://blog.csdn.net/bingbob/article/details/127583126