비디오로 화면 캡처

이 문서에서는 Windows.Graphics.Capture API를 사용하여 화면에서 캡처한 프레임을 비디오 파일로 인코딩하는 방법을 설명합니다. 정지 이미지 화면 캡처에 대한 내용은 화면 캡처를 참조하세요. 이 문서에 설명된 개념 및 기술을 활용하는 간단한 엔드투엔드 샘플 앱은 SimpleRecorder를 참조하세요.

비디오 캡처 프로세스의 개요

이 문서에서는 창의 콘텐츠를 비디오 파일에 기록하는 예제 앱 연습을 제공합니다. 이 시나리오를 구현하려면 많은 코드가 필요한 것처럼 보일 수 있지만, 화면 레코더 앱의 대략적인 구조는 매우 간단합니다. 화면 캡처 프로세스에는 다음과 같은 세 가지 기본 UWP 기능이 사용됩니다.

  • Windows.GraphicsCapture API는 실제로 화면에서 픽셀을 가져오는 작업을 수행합니다. GraphicsCaptureItem 클래스는 캡처 중인 창이나 디스플레이를 나타냅니다. GraphicsCaptureSession은 캡처 작업을 시작하고 중지하는 데 사용됩니다. Direct3D11CaptureFramePool 클래스는 화면 내용이 복사되는 프레임의 버퍼를 유지합니다.
  • MediaStreamSource 클래스는 캡처된 프레임을 받아서 비디오 스트림을 생성합니다.
  • MediaTranscoder 클래스는 MediaStreamSource에서 생성한 스트림을 받아서 비디오 파일로 인코딩합니다.

이 문서에 나와 있는 예제 코드는 다음과 같은 몇 가지 작업으로 분류할 수 있습니다.

  • 초기화 - 위에서 설명한 UWP 클래스를 구성하고, 그래픽 디바이스 인터페이스를 초기화하고, 캡처할 창을 선택하고, 해상도 및 프레임 속도와 같은 인코딩 매개 변수를 설정하는 작업이 포함됩니다.
  • 이벤트 처리기 및 스레딩 - 기본 캡처 루프의 기본 드라이버는 SampleRequested 이벤트를 통해 정기적으로 프레임을 요청하는 MediaStreamSource입니다. 이 예제에서는 이벤트를 사용하여 예제의 여러 구성 요소 간의 새 프레임에 대한 요청을 조정합니다. 동기화는 프레임을 동시에 캡처하고 인코딩하는 데 중요합니다.
  • 프레임 복사 - 인코딩할 때 리소스를 덮어쓰지 않기 위해 프레임은 캡처 프레임 버퍼에서 MediaStreamSource로 전달할 수 있는 별도의 Direct3D 화면으로 복사됩니다. Direct3D API는 이 복사 작업을 빠르게 수행하는 데 사용됩니다.

Direct3D API 정보

위에서 언급했듯이, 캡처된 각 프레임의 복사는 이 문서에서 시연하는 구현 중에서 가장 복잡한 부분입니다. 하위 수준에서는 이 작업이 Direct3D를 사용하여 수행됩니다. 이 예제에서는 SharpDX 라이브러리를 사용하여 C#에서 Direct3D 작업을 수행합니다. 이 라이브러리는 더 이상 공식적으로 지원되지 않지만, 하위 수준 복사 작업에서 성능이 이 시나리오에 적합하기 때문에 선택되었습니다. 저희는 이러한 작업에 사용할 고유의 코드나 기타 라이브러리를 쉽게 대체할 수 있도록 Direct3D 작업을 최대한 개별적으로 유지하려고 노력합니다.

프로젝트 설정

이 연습의 예제 코드는 Visual Studio 2019의 빈 앱(Universal Windows) C# 프로젝트 템플릿을 사용하여 만들었습니다. 앱에서 Windows.Graphics.Capture API를 사용하려면 프로젝트의 Package.appxmanifest 파일에 그래픽 캡처 기능을 포함해야 합니다. 이 예제에서는 생성된 비디오 파일을 디바이스의 비디오 라이브러리에 저장합니다. 이 폴더에 액세스하려면 비디오 라이브러리 기능을 포함해야 합니다.

SharpDX Nuget 패키지를 설치하려면 Visual Studio에서 Nuget 패키지 관리를 선택합니다. 찾아보기 탭에서 "SharpDX.Direct3D11" 패키지를 검색하고 설치를 클릭합니다.

이 문서에 있는 코드 목록의 크기를 줄이기 위해 아래 연습의 코드에서는 명시적 네임스페이스 참조와 선행 밑줄("_")로 명명된 MainPage 클래스 멤버 변수의 선언을 생략합니다.

인코딩 설정

이 섹션에서 설명하는 SetupEncoding 메서드는 비디오 프레임을 캡처하고 인코딩하는 데 사용될 기본 개체를 초기화하고, 캡처된 비디오의 인코딩 매개 변수를 설정합니다. 이 메서드는 프로그래밍 방식으로 또는 단추 클릭과 같은 사용자 조작에 대한 응답으로 호출할 수 있습니다. SetupEncoding에 대한 코드 목록은 아래에서 초기화 단계에 대한 설명 뒤에 표시됩니다.

  • 캡처 지원을 확인합니다. 캡처 프로세스를 시작하기 전에 GraphicsCaptureSession.IsSupported를 호출하여 화면 캡처 기능이 현재 디바이스에서 지원되는지 확인해야 합니다.

  • Direct3D 인터페이스를 초기화합니다. 이 샘플에서는 Direct3D를 사용하여 화면에서 캡처한 픽셀을 비디오 프레임으로 인코딩된 텍스처에 복사합니다. Direct3D 인터페이스를 초기화하는 데 사용되는 도우미 메서드 CreateD3DDeviceCreateSharpDXDevice는 이 문서의 뒷부분에서 다룹니다.

  • GraphicsCaptureItem을 초기화합니다. GraphicsCaptureItem은 화면(창 또는 전체 화면)에서 캡처할 항목을 나타냅니다. 사용자가 GraphicsCapturePicker를 만들고 PickSingleItemAsync를 호출하여 캡처할 항목을 선택할 수 있습니다.

  • 컴퍼지션 텍스처를 만듭니다. 각 비디오 프레임을 복사하는 데 사용할 텍스처 리소스 및 관련된 렌더링 대상 보기를 만듭니다. GraphicsCaptureItem을 만들고 해당 치수를 알기 전에는 이 텍스처를 만들 수 없습니다. 이 컴퍼지션 텍스처의 사용 방법은 WaitForNewFrame 설명을 참조하세요. 이 텍스처를 만드는 데 사용되는 도우미 메서드 역시 이 문서의 뒷부분에서 다룹니다.

  • MediaEncodingProfile 및 VideoStreamDescriptor를 만듭니다. MediaStreamSource 클래스의 인스턴스는 화면에서 캡처한 이미지를 가져와 비디오 스트림으로 인코딩합니다. 그러면 비디오 스트림이 MediaTranscoder 클래스를 통해 비디오 파일로 트랜스코딩됩니다. VideoStreamDecriptorMediaStreamSource에 대한 해상도 및 프레임 속도와 같은 인코딩 매개 변수를 제공합니다. MediaTranscoder에 대한 비디오 파일 인코딩 매개 변수는 MediaEncodingProfile에서 지정합니다. 비디오 인코딩에 사용되는 크기가 캡처되는 창의 크기와 같을 필요는 없지만, 이 예제를 단순하게 유지하기 위해 인코딩 설정은 캡처 항목의 실제 치수를 사용하도록 하드 코딩됩니다.

  • MediaStreamSource 및 MediaTranscoder 개체를 만듭니다. 위에서 언급했듯이, MediaStreamSource 개체는 개별 프레임을 비디오 스트림으로 인코딩합니다. 이 클래스의 생성자를 호출하고, 이전 단계에서 만든 MediaEncodingProfile을 전달합니다. 버퍼 시간을 0으로 설정하고 StartingSampleRequested 이벤트에 대한 처리기를 등록합니다. 처리기에 대한 내용은 이 문서의 뒷부분에 나옵니다. 다음으로, MediaTranscoder 클래스의 새 인스턴스를 만들고 하드웨어 가속을 사용하도록 설정합니다.

  • 출력 파일 만들기 이 메서드의 마지막 단계는 비디오를 트랜스코딩할 파일을 만드는 것입니다. 이 예제에서는 디바이스의 Videos Library 폴더에 고유한 이름의 파일을 만들겠습니다. 이 폴더에 액세스하려면 앱이 앱 매니페스트에서 "비디오 라이브러리" 기능을 지정해야 합니다. 파일이 만들어지면 읽고 쓸 수 있도록 파일을 열고 결과 스트림을 다음 예제의 EncodeAsync 메서드에 전달합니다.

private async Task SetupEncoding()
{
    if (!GraphicsCaptureSession.IsSupported())
    {
        // Show message to user that screen capture is unsupported
        return;
    }

    // Create the D3D device and SharpDX device
    if (_device == null)
    {
        _device = Direct3D11Helpers.CreateD3DDevice();
    }
    if (_sharpDxD3dDevice == null)
    {
        _sharpDxD3dDevice = Direct3D11Helpers.CreateSharpDXDevice(_device);
    }
    


    try
    {
        // Let the user pick an item to capture
        var picker = new GraphicsCapturePicker();
        _captureItem = await picker.PickSingleItemAsync();
        if (_captureItem == null)
        {
            return;
        }

        // Initialize a blank texture and render target view for copying frames, using the same size as the capture item
        _composeTexture = Direct3D11Helpers.InitializeComposeTexture(_sharpDxD3dDevice, _captureItem.Size);
        _composeRenderTargetView = new SharpDX.Direct3D11.RenderTargetView(_sharpDxD3dDevice, _composeTexture);

        // This example encodes video using the item's actual size.
        var width = (uint)_captureItem.Size.Width; 
        var height = (uint)_captureItem.Size.Height;

        // Make sure the dimensions are are even. Required by some encoders.
        width = (width % 2 == 0) ? width : width + 1;
        height = (height % 2 == 0) ? height : height + 1;


        var temp = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p);
        var bitrate = temp.Video.Bitrate;
        uint framerate = 30;

        _encodingProfile = new MediaEncodingProfile();
        _encodingProfile.Container.Subtype = "MPEG4";
        _encodingProfile.Video.Subtype = "H264";
        _encodingProfile.Video.Width = width;
        _encodingProfile.Video.Height = height;
        _encodingProfile.Video.Bitrate = bitrate;
        _encodingProfile.Video.FrameRate.Numerator = framerate;
        _encodingProfile.Video.FrameRate.Denominator = 1;
        _encodingProfile.Video.PixelAspectRatio.Numerator = 1;
        _encodingProfile.Video.PixelAspectRatio.Denominator = 1;

        var videoProperties = VideoEncodingProperties.CreateUncompressed(MediaEncodingSubtypes.Bgra8, width, height);
        _videoDescriptor = new VideoStreamDescriptor(videoProperties);

        // Create our MediaStreamSource
        _mediaStreamSource = new MediaStreamSource(_videoDescriptor);
        _mediaStreamSource.BufferTime = TimeSpan.FromSeconds(0);
        _mediaStreamSource.Starting += OnMediaStreamSourceStarting;
        _mediaStreamSource.SampleRequested += OnMediaStreamSourceSampleRequested;

        // Create our transcoder
        _transcoder = new MediaTranscoder();
        _transcoder.HardwareAccelerationEnabled = true;


        // Create a destination file - Access to the VideosLibrary requires the "Videos Library" capability
        var folder = KnownFolders.VideosLibrary;
        var name = DateTime.Now.ToString("yyyyMMdd-HHmm-ss");
        var file = await folder.CreateFileAsync($"{name}.mp4");
        
        using (var stream = await file.OpenAsync(FileAccessMode.ReadWrite))

        await EncodeAsync(stream);
        
    }
    catch (Exception ex)
    {
        
        return;
    }
}

인코딩 시작

기본 개체가 초기화되었으므로, 캡처 작업을 시작하도록 EncodeAsync 메서드가 구현됩니다. 이 메서드는 기록할 준비가 되었는지 먼저 확인합니다. 아직 준비되지 않았으면 도우미 메서드 StartCapture를 호출하여 화면에서 프레임 캡처를 시작합니다. 이 메서드는 이 문서의 뒷부분에서 다룹니다. 다음으로, PrepareMediaStreamSourceTranscodeAsync를 호출하여 이전 섹션에서 만든 인코딩 프로필을 사용하여 MediaStreamSource 개체를 통해 생성된 비디오 스트림을 출력 파일 스트림으로 트랜스코딩하도록 MediaTranscoder를 준비합니다. 트랜스코더가 준비되면 TranscodeAsync를 호출하여 트랜스코딩을 시작합니다. MediaTranscoder 사용에 대한 자세한 내용은 미디어 파일 트랜스코딩을 참조하세요.


private async Task EncodeAsync(IRandomAccessStream stream)
{
    if (!_isRecording)
    {
        _isRecording = true;

        StartCapture();

        var transcode = await _transcoder.PrepareMediaStreamSourceTranscodeAsync(_mediaStreamSource, stream, _encodingProfile);

        await transcode.TranscodeAsync();
    }
}

MediaStreamSource 이벤트 처리

MediaStreamSource 개체는 화면에서 캡처한 프레임을 가져와 MediaTranscoder를 사용하여 파일에 저장할 수 있는 비디오 스트림으로 변환합니다. 개체의 이벤트에 대한 처리기를 통해 MediaStreamSource에 프레임을 전달합니다.

SampleRequested 이벤트는 MediaStreamSource가 새 비디오 프레임에 대한 준비를 마쳤을 때 발생합니다. 현재 기록 중인 것이 확인되면 도우미 메서드 WaitForNewFrame을 호출하여 화면에서 캡처한 새 프레임을 가져옵니다. 이 문서의 뒷부분에서 다루는 이 메서드는 캡처한 프레임을 포함하고 있는 ID3D11Surface 개체를 반환합니다. 이 예제에서는 마찬가지로 프레임이 캡처된 시스템 시간을 저장하는 도우미 클래스에서 IDirect3DSurface 인터페이스를 래핑합니다. 프레임과 시스템 시간은 모두 MediaStreamSample.CreateFromDirect3D11Surface 팩터리 메서드에 전달되고 결과 MediaStreamSampleMediaStreamSourceSampleRequestedEventArgsMediaStreamSourceSampleRequest.Sample 속성으로 설정됩니다. 캡처된 프레임은 이 방법으로 MediaStreamSource에 제공됩니다.

private void OnMediaStreamSourceSampleRequested(MediaStreamSource sender, MediaStreamSourceSampleRequestedEventArgs args)
{
    if (_isRecording && !_closed)
    {
        try
        {
            using (var frame = WaitForNewFrame())
            {
                if (frame == null)
                {
                    args.Request.Sample = null;
                    Stop();
                    Cleanup();
                    return;
                }

                var timeStamp = frame.SystemRelativeTime;

                var sample = MediaStreamSample.CreateFromDirect3D11Surface(frame.Surface, timeStamp);
                args.Request.Sample = sample;
            }
        }
        catch (Exception e)
        {
            Debug.WriteLine(e.Message);
            Debug.WriteLine(e.StackTrace);
            Debug.WriteLine(e);
            args.Request.Sample = null;
            Stop();
            Cleanup();
        }
    }
    else
    {
        args.Request.Sample = null;
        Stop();
        Cleanup();
    }
}

Starting 이벤트의 처리기에서 WaitForNewFrame을 호출하지만, 프레임이 캡처된 시스템 시간만 MediaStreamSourceStartingRequest.SetActualStartPosition 메서드에 전달합니다. MediaStreamSource는 이 메서드를 후속 프레임의 타이밍을 올바르게 인코딩하는 데 사용합니다.

private void OnMediaStreamSourceStarting(MediaStreamSource sender, MediaStreamSourceStartingEventArgs args)
{
    using (var frame = WaitForNewFrame())
    {
        args.Request.SetActualStartPosition(frame.SystemRelativeTime);
    }
}

캡처 시작

이 단계에 나오는 StartCapture 메서드는 이전 단계에서 나온 EncodeAsync 도우미 메서드에서 호출됩니다. 먼저 이 메서드는 캡처 작업 흐름을 제어하는 데 사용되는 이벤트 개체 세트를 초기화합니다.

  • _multithread는 복사되는 동안 다른 스레드가 SharpDX 텍스처에 액세스하지 못하게 하는 데 사용되는 SharpDX 라이브러리의 다중 스레드 개체를 래핑하는 도우미 클래스입니다.
  • _frameEvent는 새 프레임이 캡처되었으며 MediaStreamSource에 전달할 수 있다는 것을 알리는 데 사용됩니다.
  • _closedEvent는 기록이 중지되었으며 새 프레임을 기다리지 않아도 된다는 것을 알립니다.

프레임 이벤트와 종료된 이벤트는 배열에 추가되므로 캡처 루프에서 둘 중 하나를 기다릴 수 있습니다.

StartCapture 메서드의 나머지 부분에서는 실제 화면 캡처를 수행할 Windows.Graphics.Capture API를 설정합니다. 먼저 CaptureItem.Closed 이벤트에 대한 이벤트가 등록됩니다. 다음으로, 캡처된 여러 프레임을 동시에 버퍼링할 수 있는 Direct3D11CaptureFramePool이 만들어집니다. CreateFreeThreaded 메서드는 FrameArrived 이벤트가 앱의 주 스레드가 아닌 풀의 자체 작업자 스레드에서 호출되도록 프레임 풀을 만드는 데 사용됩니다. 다음으로, FrameArrived 이벤트의 처리기가 등록됩니다. 마지막으로, 선택한 CaptureItem에 대한 GraphicsCaptureSession이 만들어지고 StartCapture를 호출하여 프레임 캡처가 시작됩니다.

public void StartCapture()
{

    _multithread = _sharpDxD3dDevice.QueryInterface<SharpDX.Direct3D11.Multithread>();
    _multithread.SetMultithreadProtected(true);
    _frameEvent = new ManualResetEvent(false);
    _closedEvent = new ManualResetEvent(false);
    _events = new[] { _closedEvent, _frameEvent };

    _captureItem.Closed += OnClosed;
    _framePool = Direct3D11CaptureFramePool.CreateFreeThreaded(
        _device,
        DirectXPixelFormat.B8G8R8A8UIntNormalized,
        1,
        _captureItem.Size);
    _framePool.FrameArrived += OnFrameArrived;
    _session = _framePool.CreateCaptureSession(_captureItem);
    _session.StartCapture();
}

그래픽 캡처 이벤트 처리

이전 단계에서는 그래픽 캡처 이벤트에 대한 처리기 2개를 등록하고 캡처 루프의 흐름을 관리하는 데 도움이 되도록 몇 가지 이벤트를 설정했습니다.

FrameArrived 이벤트는 Direct3D11CaptureFramePool에 캡처된 새 프레임이 있을 때 발생합니다. 이 이벤트에 대한 처리기에서, 발신자에 대해 TryGetNextFrame을 호출하여 캡처된 다음 프레임을 가져옵니다. 프레임을 검색한 후에는 새 프레임이 있다는 것을 캡처 루프가 알 수 있도록 _frameEvent를 설정합니다.

private void OnFrameArrived(Direct3D11CaptureFramePool sender, object args)
{
    _currentFrame = sender.TryGetNextFrame();
    _frameEvent.Set();
}

Closed 이벤트 처리기에서, 중지할 시기를 캡처 루프가 알 수 있도록 _closedEvent에 신호를 보냅니다.

private void OnClosed(GraphicsCaptureItem sender, object args)
{
    _closedEvent.Set();
}

새 프레임 대기

이 섹션에서 설명하는 WaitForNewFrame 도우미 메서드는 캡처 루프에서 어려운 부분이 발생하는 위치입니다. 이 메서드는 MediaStreamSource가 비디오 스트림에 새 프레임을 추가할 준비가 될 때마다 OnMediaStreamSourceSampleRequested 이벤트 처리기에서 호출됩니다. 간략하게 설명하자면, 이 함수는 새 프레임을 캡처하는 동안 MediaStreamSource로 전달하여 인코딩할 수 있도록 각 화면 캡처 비디오 프레임을 한 Direct3D 표면에서 다른 Direct3D 표면으로 복사하기만 합니다. 이 예제에서는 SharpDX 라이브러리를 사용하여 실제 복사 작업을 수행합니다.

새 프레임을 대기하기 전에, 이 메서드는 클래스 변수 _currentFrame에 저장된 이전 프레임을 삭제하고 _frameEvent를 다시 설정합니다. 그런 다음, _frameEvent 또는 _closedEvent가 신호를 받을 때까지 기다립니다. 종료된 이벤트가 설정되면 앱에서 도우미 메서드를 호출하여 캡처 리소스를 정리합니다. 이 메서드는 이 문서의 뒷부분에서 다룹니다.

프레임 이벤트가 설정되면 이전 단계에서 정의한 FrameArrived 이벤트 처리기가 호출된 것을 알 수 있으며, 캡처한 프레임 데이터를 MediaStreamSource에 전달되는 Direct3D 11 표면으로 복사하는 프로세스를 시작합니다.

이 예제에서는 도우미 클래스 SurfaceWithInfo를 사용합니다. 이 클래스는 비디오 프레임과 프레임의 시스템 시간을 전달할 수 있으며, 둘 다 MediaStreamSource에 단일 개체로 필요합니다. 프레임 복사 프로세스의 첫 번째 단계는 이 클래스를 인스턴스화하고 시스템 시간을 설정하는 것입니다.

그 다음 단계는 SharpDX 라이브러리를 사용하는 이 예제의 일부입니다. 여기에 사용되는 도우미 함수는 이 문서의 마지막 부분에 정의되어 있습니다. 먼저 MultiThreadLock을 사용하여 복사를 수행하는 동안 다른 스레드가 비디오 프레임 버퍼에 액세스할 수 없게 합니다. 다음으로, 도우미 메서드 CreateSharpDXTexture2D를 호출하여 비디오 프레임에서 SharpDX Texture2D 개체를 만듭니다. 이것은 복사 작업의 원본 텍스처가 됩니다.

다음으로, 이전 단계에서 만든 Texture2D 개체를 이 프로세스의 앞부분에서 만든 컴퍼지션 텍스처에 복사합니다. 이 컴퍼지션 텍스처는 다음 프레임이 캡처될 때 인코딩 프로세스가 픽셀에 대해 작동할 수 있도록 스왑 버퍼 역할을 합니다. 복사를 수행하기 위해 컴퍼지션 텍스처와 연결된 렌더링 대상 보기를 지우고, 텍스처 내에서 복사하려는 영역(이 예에서는 전체 텍스처)을 정의한 다음, CopySubresourceRegion을 호출하여 실제로 픽셀을 컴포지션 텍스처에 복사합니다.

대상 텍스처를 만들 때 사용할 텍스처 설명의 복사본을 만들지만, 설명이 수정되고 새 텍스처가 쓰기 액세스 권한을 갖도록 BindFlagsRenderTarget으로 설정됩니다. CpuAccessFlagsNone으로 설정하면 시스템에서 복사 작업을 최적화할 수 있습니다. 텍스처 설명은 새 텍스처 리소스를 만드는 데 사용되고 컴퍼지션 텍스처 리소스는 CopyResource를 호출하여 이 새 리소스에 복사됩니다. 마지막으로, 이 메서드에서 반환되는 IDirect3DSurface 개체를 만들기 위해 CreateDirect3DSurfaceFromSharpDXTexture가 호출됩니다.

public SurfaceWithInfo WaitForNewFrame()
{
    // Let's get a fresh one.
    _currentFrame?.Dispose();
    _frameEvent.Reset();

    var signaledEvent = _events[WaitHandle.WaitAny(_events)];
    if (signaledEvent == _closedEvent)
    {
        Cleanup();
        return null;
    }

    var result = new SurfaceWithInfo();
    result.SystemRelativeTime = _currentFrame.SystemRelativeTime;
    using (var multithreadLock = new MultithreadLock(_multithread))
    using (var sourceTexture = Direct3D11Helpers.CreateSharpDXTexture2D(_currentFrame.Surface))
    {

        _sharpDxD3dDevice.ImmediateContext.ClearRenderTargetView(_composeRenderTargetView, new SharpDX.Mathematics.Interop.RawColor4(0, 0, 0, 1));

        var width = Math.Clamp(_currentFrame.ContentSize.Width, 0, _currentFrame.Surface.Description.Width);
        var height = Math.Clamp(_currentFrame.ContentSize.Height, 0, _currentFrame.Surface.Description.Height);
        var region = new SharpDX.Direct3D11.ResourceRegion(0, 0, 0, width, height, 1);
        _sharpDxD3dDevice.ImmediateContext.CopySubresourceRegion(sourceTexture, 0, region, _composeTexture, 0);

        var description = sourceTexture.Description;
        description.Usage = SharpDX.Direct3D11.ResourceUsage.Default;
        description.BindFlags = SharpDX.Direct3D11.BindFlags.ShaderResource | SharpDX.Direct3D11.BindFlags.RenderTarget;
        description.CpuAccessFlags = SharpDX.Direct3D11.CpuAccessFlags.None;
        description.OptionFlags = SharpDX.Direct3D11.ResourceOptionFlags.None;

        using (var copyTexture = new SharpDX.Direct3D11.Texture2D(_sharpDxD3dDevice, description))
        {
            _sharpDxD3dDevice.ImmediateContext.CopyResource(_composeTexture, copyTexture);
            result.Surface = Direct3D11Helpers.CreateDirect3DSurfaceFromSharpDXTexture(copyTexture);
        }
    }

    return result;
}

캡처 중지 및 리소스 정리

Stop 메서드는 캡처 작업을 중지하는 방법을 제공합니다. 이 메서드는 앱에서 프로그래밍 방식으로 호출할 수도 있고 단추 클릭과 같은 사용자 조작에 대한 응답으로 호출할 수도 있습니다. 이 메서드는 단순히 _closedEvent를 설정합니다. 이전 단계에서 정의한 WaitForNewFrame 메서드는 이 이벤트를 검색하고, 이 이벤트가 설정되어 있으면 캡처 작업을 종료합니다.

private void Stop()
{
    _closedEvent.Set();
}

Cleanup 메서드는 복사 작업 중에 만들어진 리소스를 적절하게 삭제하는 데 사용됩니다. 다음 내용이 포함됩니다.

  • 캡처 세션에서 사용되는 Direct3D11CaptureFramePool 개체
  • GraphicsCaptureSessionGraphicsCaptureItem
  • Direct3D 및 SharpDX 디바이스
  • 복사 작업에 사용되는 SharpDX 텍스처 및 렌더링 대상 보기
  • 현재 프레임을 저장하는 데 사용되는 Direct3D11CaptureFrame
private void Cleanup()
{
    _framePool?.Dispose();
    _session?.Dispose();
    if (_captureItem != null)
    {
        _captureItem.Closed -= OnClosed;
    }
    _captureItem = null;
    _device = null;
    _sharpDxD3dDevice = null;
    _composeTexture?.Dispose();
    _composeTexture = null;
    _composeRenderTargetView?.Dispose();
    _composeRenderTargetView = null;
    _currentFrame?.Dispose();
}

도우미 래퍼 클래스

다음 도우미 클래스는 이 문서의 예제 코드에 도움을 주도록 정의되었습니다.

MultithreadLock 도우미 클래스는 복사가 진행되는 동안 다른 스레드가 텍스처 리소스에 액세스하지 못하게 하는 SharpDX Multithread 클래스를 래핑합니다.

class MultithreadLock : IDisposable
{
    public MultithreadLock(SharpDX.Direct3D11.Multithread multithread)
    {
        _multithread = multithread;
        _multithread?.Enter();
    }

    public void Dispose()
    {
        _multithread?.Leave();
        _multithread = null;
    }

    private SharpDX.Direct3D11.Multithread _multithread;
}

SurfaceWithInfo는 각각 캡처된 프레임과 캡처된 시간을 나타내는 IDirect3DSurfaceSystemRelativeTime을 연결하는 데 사용됩니다.

public sealed class SurfaceWithInfo : IDisposable
{
    public IDirect3DSurface Surface { get; internal set; }
    public TimeSpan SystemRelativeTime { get; internal set; }

    public void Dispose()
    {
        Surface?.Dispose();
        Surface = null;
    }
}

Direct3D 및 SharpDX 도우미 API

다음 도우미 API는 Direct3D 및 SharpDX 리소스 생성을 추상화하도록 정의되어 있습니다. 이러한 기술에 대한 자세한 설명은 이 문서의 범위를 벗어나지만, 연습에 사용된 예제 코드를 구현해 볼 수 있도록 여기에 코드를 제공합니다.

[ComImport]
[Guid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComVisible(true)]
interface IDirect3DDxgiInterfaceAccess
{
    IntPtr GetInterface([In] ref Guid iid);
};

public static class Direct3D11Helpers
{
    internal static Guid IInspectable = new Guid("AF86E2E0-B12D-4c6a-9C5A-D7AA65101E90");
    internal static Guid ID3D11Resource = new Guid("dc8e63f3-d12b-4952-b47b-5e45026a862d");
    internal static Guid IDXGIAdapter3 = new Guid("645967A4-1392-4310-A798-8053CE3E93FD");
    internal static Guid ID3D11Device = new Guid("db6f6ddb-ac77-4e88-8253-819df9bbf140");
    internal static Guid ID3D11Texture2D = new Guid("6f15aaf2-d208-4e89-9ab4-489535d34f9c");

    [DllImport(
        "d3d11.dll",
        EntryPoint = "CreateDirect3D11DeviceFromDXGIDevice",
        SetLastError = true,
        CharSet = CharSet.Unicode,
        ExactSpelling = true,
        CallingConvention = CallingConvention.StdCall
        )]
    internal static extern UInt32 CreateDirect3D11DeviceFromDXGIDevice(IntPtr dxgiDevice, out IntPtr graphicsDevice);

    [DllImport(
        "d3d11.dll",
        EntryPoint = "CreateDirect3D11SurfaceFromDXGISurface",
        SetLastError = true,
        CharSet = CharSet.Unicode,
        ExactSpelling = true,
        CallingConvention = CallingConvention.StdCall
        )]
    internal static extern UInt32 CreateDirect3D11SurfaceFromDXGISurface(IntPtr dxgiSurface, out IntPtr graphicsSurface);

    public static IDirect3DDevice CreateD3DDevice()
    {
        return CreateD3DDevice(false);
    }

    public static IDirect3DDevice CreateD3DDevice(bool useWARP)
    {
        var d3dDevice = new SharpDX.Direct3D11.Device(
            useWARP ? SharpDX.Direct3D.DriverType.Software : SharpDX.Direct3D.DriverType.Hardware,
            SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport);
        IDirect3DDevice device = null;

        // Acquire the DXGI interface for the Direct3D device.
        using (var dxgiDevice = d3dDevice.QueryInterface<SharpDX.DXGI.Device3>())
        {
            // Wrap the native device using a WinRT interop object.
            uint hr = CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.NativePointer, out IntPtr pUnknown);

            if (hr == 0)
            {
                device = Marshal.GetObjectForIUnknown(pUnknown) as IDirect3DDevice;
                Marshal.Release(pUnknown);
            }
        }

        return device;
    }


    internal static IDirect3DSurface CreateDirect3DSurfaceFromSharpDXTexture(SharpDX.Direct3D11.Texture2D texture)
    {
        IDirect3DSurface surface = null;

        // Acquire the DXGI interface for the Direct3D surface.
        using (var dxgiSurface = texture.QueryInterface<SharpDX.DXGI.Surface>())
        {
            // Wrap the native device using a WinRT interop object.
            uint hr = CreateDirect3D11SurfaceFromDXGISurface(dxgiSurface.NativePointer, out IntPtr pUnknown);

            if (hr == 0)
            {
                surface = Marshal.GetObjectForIUnknown(pUnknown) as IDirect3DSurface;
                Marshal.Release(pUnknown);
            }
        }

        return surface;
    }



    internal static SharpDX.Direct3D11.Device CreateSharpDXDevice(IDirect3DDevice device)
    {
        var access = (IDirect3DDxgiInterfaceAccess)device;
        var d3dPointer = access.GetInterface(ID3D11Device);
        var d3dDevice = new SharpDX.Direct3D11.Device(d3dPointer);
        return d3dDevice;
    }

    internal static SharpDX.Direct3D11.Texture2D CreateSharpDXTexture2D(IDirect3DSurface surface)
    {
        var access = (IDirect3DDxgiInterfaceAccess)surface;
        var d3dPointer = access.GetInterface(ID3D11Texture2D);
        var d3dSurface = new SharpDX.Direct3D11.Texture2D(d3dPointer);
        return d3dSurface;
    }


    public static SharpDX.Direct3D11.Texture2D InitializeComposeTexture(
        SharpDX.Direct3D11.Device sharpDxD3dDevice,
        SizeInt32 size)
    {
        var description = new SharpDX.Direct3D11.Texture2DDescription
        {
            Width = size.Width,
            Height = size.Height,
            MipLevels = 1,
            ArraySize = 1,
            Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm,
            SampleDescription = new SharpDX.DXGI.SampleDescription()
            {
                Count = 1,
                Quality = 0
            },
            Usage = SharpDX.Direct3D11.ResourceUsage.Default,
            BindFlags = SharpDX.Direct3D11.BindFlags.ShaderResource | SharpDX.Direct3D11.BindFlags.RenderTarget,
            CpuAccessFlags = SharpDX.Direct3D11.CpuAccessFlags.None,
            OptionFlags = SharpDX.Direct3D11.ResourceOptionFlags.None
        };
        var composeTexture = new SharpDX.Direct3D11.Texture2D(sharpDxD3dDevice, description);
       

        using (var renderTargetView = new SharpDX.Direct3D11.RenderTargetView(sharpDxD3dDevice, composeTexture))
        {
            sharpDxD3dDevice.ImmediateContext.ClearRenderTargetView(renderTargetView, new SharpDX.Mathematics.Interop.RawColor4(0, 0, 0, 1));
        }

        return composeTexture;
    }
}

참고 항목