Microsoft MVP성태의 닷넷 이야기
VC++: 121. DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++) [링크 복사], [링크+제목 복사]
조회: 7314
글쓴 사람
홈페이지
첨부 파일

DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++)

화면 캡처 글을 검색해 보면 다양한 글이 나옵니다.

Various methods for capturing the screen
; http://www.codeproject.com/Articles/5051/Various-methods-for-capturing-the-screen

일단 위의 글에서는 3가지 방법을 소개하는데 그중 DirectX 예제만 따라 해 보았지만 Windows 10에서 동작하지 않았습니다. 왜냐하면 GetFrontBufferData 호출에서,

hr = g_pd3dDevice->GetFrontBufferData(0, pSurface);
// hr == 0x8876086c (D3DERR_INVALIDCALL)

D3DERR_INVALIDCALL 오류가 발생하는데 이유를 모르겠습니다. (혹시 아시는 분 덧글 부탁드립니다.)

게다가 d3dx9tex.h 헤더 파일 및 d3dx9.lib 파일이 없다고 오류가 발생하는데 이를 정상적으로 컴파일하기 위해서는 다음의 과정을 거쳐야 합니다.

DXSDK_Jun10.exe 설치 시 "Error Code: S1023" 오류 해결하는 방법
; https://www.sysnet.pe.kr/2/0/1195

귀찮으니 ^^; 다음 캡처 방식으로 넘어가 보겠습니다. Windows 8부터 지원하는 DXGI를 이용하면,

DXGI fast screen capture 
; http://www.pavelgurenko.com/2013/12/dxgi-outputs-enumeration-and-fast.html

The second way of performing capture is DirectX Graphics Infrastructure - you command video card to store the whole screen or one/several monitors's contents inside separate part of its memory (surface).


속도는 물론이고 일반적인 GDI API로는 할 수 없는 DirectX 게임 화면까지도 안정적으로 캡처할 수 있다고 합니다. 친절하게도 위의 블로그를 쓴 사람이 github에 소스 코드도 공개한 것이 있고,

pgurenko/DXGICaptureSample 
; https://github.com/pgurenko/DXGICaptureSample

또한 다음과 같은 식으로 사용하면, 3840 * 2160 해상도에서 30 fps로 캡처할 수 있었다고 합니다.

// https://github.com/pgurenko/DXGICaptureSample/issues/2

#include "stdafx.h"
#include "DXGIManager.h"
#include <time.h>

DXGIManager g_DXGIManager;

int capture(RECT& rcDim, vector<BYTE>& buf, CComPtr<IWICImagingFactory>& spWICFactory) {
  DWORD dwWidth = rcDim.right - rcDim.left;
  DWORD dwHeight = rcDim.bottom - rcDim.top;
  DWORD dwBufSize = buf.size();

  HRESULT hr = g_DXGIManager.GetOutputBits(buf.data(), rcDim);
  if (FAILED(hr))
  {
    printf("GetOutputBits failed with hr=0x%08x\n", hr);
    return hr;
  }
  return 0;
}

int _tmain(int argc, _TCHAR* argv[]) {
  CoInitialize(NULL);

  g_DXGIManager.SetCaptureSource(CSDesktop);

  RECT rcDim;
  g_DXGIManager.GetOutputRect(rcDim);

  DWORD dwWidth = rcDim.right - rcDim.left;
  DWORD dwHeight = rcDim.bottom - rcDim.top;

  printf("dwWidth=%d dwHeight=%d\n", dwWidth, dwHeight);

  DWORD dwBufSize = dwWidth*dwHeight * 4;

  vector<BYTE> buf(dwBufSize);

  CComPtr<IWICImagingFactory> spWICFactory = NULL;
  HRESULT hr = spWICFactory.CoCreateInstance(CLSID_WICImagingFactory);
  if (FAILED(hr))
    return hr;

  clock_t t1 = clock();
  int i;
  int iterations = 100;  
  for (i = 0; i < iterations; i++) {
    capture(rcDim, buf, spWICFactory);
  }
  clock_t t2 = clock();
  printf("%d iterations: %0.0f fps\n", iterations, iterations / ((double)(t2 - t1) / CLOCKS_PER_SEC));

  return 0;
}

하지만 실제로 해보면 iterations 중에 capture 함수 내의 "GetOutputBits failed with hr=0x887a0027" 호출 실패가 있기 때문에 이를 감안하면 성능이 더 낮을 것입니다. 그래도 이 정도면 훌륭하죠. ^^




참고로, 대충 보니까 중요한 코드는 DXGIOutputDuplication::AcquireNextFrame 내의 AcquireNextFrame 호출이었습니다.

HRESULT DXGIOutputDuplication::AcquireNextFrame(IDXGISurface1** pDXGISurface, DXGIPointerInfo*& pDXGIPointer)
{
    DXGI_OUTDUPL_FRAME_INFO fi;
    CComPtr<IDXGIResource> spDXGIResource;

    HRESULT hr = m_DXGIOutputDuplication->AcquireNextFrame(50, &fi, &spDXGIResource);
    if(FAILED(hr))
    {
        __L_INFO("m_DXGIOutputDuplication->AcquireNextFrame failed with hr=0x%08x", hr);
        return hr;
    }

    // ... [생략] ...
}

인자로 전달한 50은 50ms 시간 내에서 다음 화면 데이터를 얻어내라는 것입니다. 만약 그 시간 내에 데이터를 얻을 수 없었다면 hr == 0x887a0027 (The timeout value has elapsed and the resource is not yet available) 오류가 발생합니다. 따라서 저 시간 값을 너무 낮추면 호출에 대한 오류가 쓸데없이 많이 발생할 수 있습니다.

그 외에, 모니터 3대인 경우에는 CSMonitor3 스위치 문을 다음과 같이 DXGIManager::GetOutputDuplication 함수에 추가하면 됩니다.

case CSMonitor3:
{
    int secondary = 0;
    // Return the first with !IsPrimary
    for (vector<DXGIOutputDuplication>::iterator iter = m_vOutputs.begin();
        iter != m_vOutputs.end();
        iter++)
    {
        DXGIOutputDuplication& out = *iter;
        if (!out.IsPrimary())
        {
            if (secondary == 0)
            {
                secondary = 1;
                continue;
            }

            outputs.push_back(out);
            break;
        }
    }
}
break;




위의 소개 글에서 pgurenko/DXGICaptureSample로 공개된 캡처 예제를 버퍼 그대로 Bitmap으로 바꿔 화면에 출력하는 코드를 다음과 같이 작성해 봤습니다.

#include "stdafx.h"
#include "DXGIManager.h"
#include <time.h>

DXGIManager g_DXGIManager;

LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

WNDCLASSEX g_wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L, GetModuleHandle(NULL), NULL, NULL, NULL, NULL, L"Foo", NULL };
HWND g_hWnd;
HBITMAP g_hbm;
int hDC;
int g_screenX, g_screenY;

int _tmain(int argc, _TCHAR* argv[])
{
    CoInitialize(NULL);

    g_DXGIManager.SetCaptureSource(CaptureSource::CSMonitor1);

    RECT rcDim;
    g_DXGIManager.GetOutputRect(rcDim);

    DWORD dwWidth = rcDim.right - rcDim.left;
    DWORD dwHeight = rcDim.bottom - rcDim.top;

    g_screenX = dwWidth;
    g_screenY = dwHeight;

    RegisterClassEx(&g_wc);
    g_hWnd = CreateWindow(L"Foo", L"Foo", WS_OVERLAPPEDWINDOW, 0, 0, dwWidth, dwHeight, NULL, NULL, g_wc.hInstance, NULL);
    ::ShowWindow(g_hWnd, SW_SHOW);

    printf("dwWidth=%d dwHeight=%d\n", dwWidth, dwHeight);

    HDC hdcWin = GetDC(g_hWnd);
    RECT rc = { 0, 0, dwWidth, dwHeight };
    BITMAPINFO bmi = { 0 };
    bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
    bmi.bmiHeader.biWidth = dwWidth;
    bmi.bmiHeader.biHeight = -dwHeight;
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;
    bmi.bmiHeader.biCompression = BI_RGB;
    RGBQUAD *prgbBits;
    g_hbm = CreateDIBSection(hdcWin, &bmi, DIB_RGB_COLORS, &reinterpret_cast<void*&>(prgbBits), NULL, 0);

    DWORD dwBufSize = dwWidth*dwHeight * 4;

    vector<BYTE> buf(dwBufSize);
    BYTE *pBuf = buf.data();

    MSG msg;

    while (1)
    {
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) == TRUE)
        {
            if (msg.message == WM_QUIT)
            {
                break;
            }

            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            HRESULT hr = g_DXGIManager.GetOutputBits(pBuf, rcDim);

            if (hr != S_OK)
            {
                continue;
            }

            memcpy(prgbBits, pBuf, dwWidth * dwHeight * 4);
            ::InvalidateRect(g_hWnd, nullptr, TRUE);
        }
    }

    CoUninitialize();

    return msg.wParam;
}

LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    if (msg == WM_CREATE)
    {
        return 0;
    }

    if (msg == WM_PAINT)
    {
        PAINTSTRUCT paint;
        HDC hDC = ::BeginPaint(hWnd, &paint);

        HDC hdcMem = CreateCompatibleDC(hDC);
        HBITMAP hbmPrev = (HBITMAP)::SelectObject(hdcMem, g_hbm);

        BitBlt(hDC, 0, 0, g_screenX, g_screenY, hdcMem, 0, 0, SRCCOPY);
        
        SelectObject(hdcMem, hbmPrev);
        DeleteDC(hdcMem);

        ::EndPaint(hWnd, &paint);
        return 0;
    }

    return DefWindowProc(hWnd, msg, wParam, lParam);
}

첨부 파일은 위의 예제 코드를 포함합니다.

실제로 실행해서 프로그램을 돌려보면... 부드러운 캡처 성능에 반하실 것입니다. ^^




시간 있으시면 다음의 글도 보시면 좋겠지요. ^^

Desktop Screen Capture on Windows via Windows Desktop Duplication API with Drawing of Cursor's Image
; https://www.codeproject.com/Tips/1116253/Desktop-Screen-Capture-on-Windows-via-Windows-Desk

DXGI desktop duplication sample
; https://code.msdn.microsoft.com/windowsdesktop/Desktop-Duplication-Sample-da4c696a




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 12/14/2017 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 



2018-06-17 05시05분
[지팡이] 잘 읽고갑니당
[손님]
2019-03-27 04시08분
[여우] DXGIManager에서 형변환 오류가 뜨는데 어떻게 해야하나요..?
2017 사용중입니다
[손님]
2019-03-27 09시27분
DXGIManager의 어느 코드에서 형 변환 오류가 뜬다는 거죠? 그리고 이 글에 첨부한 예제 코드로 실행했을 때 발생하는 건가요?
정성태

... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
11450정성태11/15/20188382Windows: 146. PowerShell로 원격 프로세스(EXE, BAT) 실행하는 방법
11449정성태3/6/20184309오류 유형: 449. 단위 테스트 - Could not load file or assembly 'Microsoft.VisualStudio.QualityTools.VideoRecorderEngine' or one of its dependencies. [1]
11448정성태1/20/20182943오류 유형: 448. Fakes를 포함한 단위 테스트 프로젝트를 빌드 시 CS0619 관련 오류 발생
11447정성태1/20/20183367.NET Framework: 730. dotnet user-secrets 명령어파일 다운로드1
11446정성태1/20/20183332.NET Framework: 729. windbg로 살펴보는 GC heap의 Segment 구조파일 다운로드1
11445정성태1/20/20182894.NET Framework: 728. windbg - 눈으로 확인하는 Workstation GC / Server GC
11444정성태9/13/20183557VS.NET IDE: 125. Visual Studio에서 Selenium WebDriver를 이용한 웹 브라우저 단위 테스트 구성파일 다운로드1
11443정성태1/18/20183726VC++: 124. libuv 모듈 살펴 보기
11442정성태1/18/20182640개발 환경 구성: 353. ASP.NET Core 프로젝트의 "Enable unmanaged code debugging" 옵션 켜는 방법
11441정성태1/18/20182806오류 유형: 447. ASP.NET Core 배포 오류 - Ensure that restore has run and that you have included '...' in the TargetFrameworks for your project.
11440정성태1/17/20183209.NET Framework: 727. ASP.NET의 HttpContext.Current 구현에 대응하는 ASP.NET Core의 IHttpContextAccessor/HttpContextAccessor 사용법파일 다운로드1
11439정성태8/25/20186975기타: 69. C# - CPU 100% 부하 주는 프로그램파일 다운로드1
11438정성태1/17/20183761오류 유형: 446. Error CS0234 The type or namespace name 'ITuple' does not exist in the namespace
11437정성태1/17/20183388VS.NET IDE: 124. Platform Toolset 설정에 따른 Visual C++의 헤더 파일 기본 디렉터리
11436정성태1/16/20183360개발 환경 구성: 352. ASP.NET Core (EXE) 프로세스가 IIS에서 호스팅되는 방법 - ASP.NET Core Module(AspNetCoreModule) [1]
11435정성태1/16/20184217개발 환경 구성: 351. OWIN 웹 서버(EXE)를 IIS에서 호스팅하는 방법 - HttpPlatformHandler (Reverse Proxy)파일 다운로드2
11434정성태1/15/20184284개발 환경 구성: 350. 사용자 정의 웹 서버(EXE)를 IIS에서 호스팅하는 방법 - HttpPlatformHandler (Reverse Proxy)파일 다운로드2
11433정성태1/15/20183647개발 환경 구성: 349. dotnet ef 명령어 사용을 위한 준비
11432정성태1/11/20184957.NET Framework: 726. WPF + Direct2D + SharpDX 출력 C# 예제파일 다운로드2
11431정성태1/11/20184317.NET Framework: 725. C# - 동기 방식이면서 비동기 메서드처럼 구현한 사례
11430정성태1/10/20186400.NET Framework: 724. WPF + Direct2D 출력 C# 예제 [1]파일 다운로드1
11429정성태1/9/20182755개발 환경 구성: 348. ASP.NET Core 2.1 Preview 버전 적용 방법
11428정성태1/7/20183810개발 환경 구성: 347. WinForm 프로젝트를 WPF 프로젝트 유형으로 변경하는 방법파일 다운로드1
11427정성태1/5/20183373오류 유형: 445. vcpkg 빌드 오류 - Starting the CLR failed with HRESULT 80040153
11426정성태10/19/20185077오류 유형: 444. curl로 호출할 때 발생하는 오류 정리
11425정성태1/4/20182787개발 환경 구성: 346. ASP.NET Core Web Application을 IIS에서 호스팅하는 방법 (2)
... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...