스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
한 차원 높은 ScreenCaptureKit 구현하기
ScreenCaptureKit을 통해 앱 사용자의 복잡한 화면 캡처 경험을 지원하는 방법을 확인하세요. 콘텐츠 필터 미세 조정, 프레임 메타데이터 해석, 윈도우 선택기 등 포함 가능한 여러 고급 옵션을 살펴보겠습니다. 또한 최적의 성능을 위해 스트림을 구성하는 방법을 보여드리겠습니다.
리소스
관련 비디오
WWDC23
WWDC22
-
다운로드
♪ 안녕하세요 저는 Meng Yang니다 Apple의 GPU 소프트웨어 엔지니어죠 오늘은 ScreenCaptureKit에 대한 고급 예제를 살펴보며 앱의 화면 공유 경험을 한 단계 끌어올리는 방법을 다루려 합니다 그다음에는 제 동료 Drew가 흥미로운 새 API 시연을 준비했습니다 화면 캡처는 화면 공유 앱의 핵심으로 Zoom, Google Meet SharePlay 및 인기 게임 스트리밍 서비스 Twitch 등에 사용됩니다 지난 몇 년간 우리의 일과 공부 및 협업과 사교의 방식으로 떠올랐죠 ScreenCaptureKit은 완전히 새로운 고성능 화면 캡처 프레임워크입니다 강력한 기능 세트로 처음부터 새로 개발했죠 풍부한 기능 세트에는 사용자 지정이 가능한 콘텐츠 제어가 포함됩니다 다양한 창과 앱은 물론이고 어떤 디스플레이와 조합해도 간단히 캡처할 수 있죠 화면 콘텐츠를 원본 해상도와 프레임률로 캡처할 수 있습니다 해상도와 프레임률 픽셀 형식 등 동적 스트림 속성 제어를 제공합니다 이러한 제어는 스트림을 다시 열 필요 없이 그 자리에서 수정할 수 있습니다 캡처 버퍼는 GPU 메모리의 지원으로 메모리 복사를 줄입니다 하드웨어 가속 콘텐츠 캡처와 스케일링 및 픽셀과 색상 형식 변환으로 CPU 사용량을 줄이면서 캡처의 성능은 향상시킵니다 마지막으로 비디오 및 오디오 캡처를 모두 지원합니다 시작하기에 앞서 이 강연에서는 여러분이 프레임워크의 작동 방식에 대한 기본 개념과 빌딩 블록 및 작업 흐름에 이미 익숙하다고 가정하겠습니다 'ScreenCaptureKit 소개' 세션을 참고해서 자세히 알아보세요 이번 세션에서 다룰 내용은 단일 창을 캡처하고 표시하는 방법입니다 다음은 전체 디스플레이 캡처에 화면 콘텐츠를 추가하고 디스플레이 캡처에서 콘텐츠를 제거하는 방법을 살펴본 후 다양한 사용 사례에 맞게 스트림을 구성하는 방법을 몇 가지 보여드리겠습니다 마지막으로 ScreenCaptureKit이 인기 오픈 소스 화면 캡처 앱 OBS Studio의 화면 및 오디오 캡처 경험을 어떻게 변화시켰는지에 알아보죠 첫 번째 예시로 시작하겠습니다 그리고 가장 일반적인 사용 사례일 겁니다 단일 창 캡처입니다 이번 예시에서 다룰 것은 단일 창 필터를 설정하는 방법입니다 캡처한 창의 크기가 조정되거나 가려지거나 최소화되거나 화면 밖으로 이동될 때 스트림 출력에서 어떤 상황이 일어날까요? 또한 프레임별 메타데이터를 사용하는 방법과 캡처된 창을 적절하게 표시하는 방법도 배우실 겁니다 시작해 보죠 활성화된 디스플레이와 무관하게 독립된 단일 창을 캡처하려면 단일 창 필터를 사용해서 시작하고 창 하나만 이용해 필터를 초기화할 수 있습니다 예시를 살펴보죠 필터는 단일 Safari 창을 포함하도록 구성되어 있습니다 비디오 출력은 다른 창 없이 해당 창만 포함합니다 Safari의 자식 창과 팝업 등 기타 창은 포함되지 않습니다 반면 ScreenCaptureKit의 오디오 캡처 정책은 언제나 앱 레벨에서 동작합니다 단일 창 필터를 사용하면 창에 포함된 앱의 모든 오디오 콘텐츠가 캡처됩니다 비디오 출력이 없는 창에서도 마찬가지죠 코드 샘플을 살펴보겠습니다 단일 창으로 스트림을 만들려면 SCShareableContent를 통해 공유할 수 있는 모든 콘텐츠를 가져오는 것부터 시작하세요 SCShareableContent에서 공유하려는 창을 windowID 매칭으로 가져옵니다 다음에는 명시된 SCWindow를 이용해서 desktopIndependentWindow 유형 SCContentFilter를 만듭니다 스트림 출력에 오디오를 포함하도록 스트림을 구성할 수도 있습니다 이제 contentFilter와 streamConfig를 사용해 스트림을 만들 준비가 끝났습니다 StreamOutput을 추가하고 스트림을 시작하세요 다음으로는 스트림 출력을 살펴보겠습니다 이 예시에서는 왼쪽이 소스 디스플레이고 오른쪽이 스트림 출력입니다 단일 Safari 창이 스트림 필터에 포함됩니다 이제 캡처 중인 Safari 창을 스크롤해 보겠습니다 스트림 출력은 단일 Safari 창의 실시간 콘텐츠가 포함됩니다 소스 디스플레이와 같은 작업 리듬으로 업데이트하며 이는 소스 디스플레이의 기본 프레임률까지 올라갑니다 예를 들어 소스 창이 120Hz 디스플레이에서 지속적으로 업데이트된다면 스트림 출력도 최대 120fps로 업데이트될 수 있습니다 창 크기를 조정하면 어떻게 될지 궁금하실 텐데요 주의하실 점은 스트림의 출력 크기를 자주 변경하면 추가 메모리 할당이 발생한다는 것입니다 따라서 추천하지는 않습니다 스트림의 출력 크기는 대체로 고정되어 있습니다 소스 창에 따라 크기가 조정되지 않죠 이제 소스 창의 크기를 바꾸고 스트림 출력이 어떻게 변하는지 살펴보겠습니다 ScreenCaptureKit은 항상 캡처된 창에서 하드웨어 스케일링을 수행하므로 소스 창의 크기를 조정해도 프레임 출력을 넘어서진 않습니다 창이 다른 창에 가려지면 어떨까요? 소스 창이 전체나 혹은 일부가 가려지더라도 스트림 출력에는 항상 창의 전체 콘텐츠가 모두 포함됩니다 창이 완전히 화면을 벗어나거나 다른 디스플레이로 이동한 경우에도 이 규칙은 그대로 적용됩니다 최소화된 창의 경우 소스 창이 최소화되면 스트림 출력이 일시 중지되고 창이 최소화에서 벗어나면 다시 재개됩니다 다음은 오디오 출력을 살펴보죠 이번 예시에서는 두 개의 Safari 창에 오디오 트랙이 있지만 왼쪽 창만 캡처되고 있습니다 비디오 출력은 첫 번째 창만 포함되며 양쪽 Safari 창의 두 오디오 트랙이 모두 오디오 출력에 포함됩니다 직접 보고 들어보시죠 제가 제일 좋아하는 과카몰리 레시피입니다 아보카도 네 개가 필요합니다 스트림이 시작되고 실행되면서 사용 가능한 새 프레임이 나올 때마다 앱에서 프레임 업데이트를 수신합니다 프레임 출력에 포함된 IOSurface는 캡처된 프레임과 프레임별 메타데이터를 표시하죠 잠시 메타데이터에 대해 짚고 넘어가겠습니다 여러분의 앱에 매우 유용한 메타데이터의 예시를 보여 드리겠습니다 여기에는 더티 사각형과 콘텐츠 사각형 및 콘텐츠 스케일과 스케일 팩터가 포함됩니다 더티 사각형부터 시작해 보죠 더티 사각형은 이전 프레임 대비 새 콘텐츠의 위치를 나타냅니다 이 예시에서는 강조된 더티 사각형이 업데이트되는 프레임 영역을 가리킵니다 항상 전체 프레임을 인코딩하거나 두 프레임 사이 델타를 인코더에서 계산하는 대신 간단히 더티 사각형을 사용해서 새 업데이트가 있는 영역만 인코딩 및 전송하고 수신 측의 이전 프레임에 업데이트를 복사하여 새 프레임을 생성할 수 있습니다 일치하는 키를 사용하면 출력 CMSampleBuffer의 메타데이터 사전에서 더티 사각형을 검색할 수 있죠 이제 콘텐츠 사각형과 콘텐츠 스케일로 넘어가겠습니다 캡처할 소스 창은 왼쪽에 있고 스트림 출력은 오른쪽에 표시됩니다 창의 크기를 조정할 수 있기 때문에 소스 창의 기본 배경 표면 크기가 스트림 출력 크기와 일치하지 않을 때가 많습니다 이 예시에서 캡처된 창은 프레임 출력과 비교할 때 종횡비가 다르고 더 큽니다 캡처된 창은 출력에 맞게 축소됩니다 녹색으로 강조된 콘텐츠 사각형은 스트림 출력에서 캡처된 콘텐츠의 관심 영역을 나타냅니다 콘텐츠 스케일은 콘텐츠의 크기를 맞추려고 조정한 비율을 의미합니다 여기 캡처된 Safari 창은 프레임 안에 들어가도록 0.77배로 축소됐습니다 방금 말씀드린 메타데이터를 사용해서 원래 모습과 가장 비슷하게 캡처된 창을 표시할 수 있습니다 우선 콘텐츠 사각형을 이용해 출력에서 콘텐츠를 잘라 보겠습니다 이제 콘텐츠 스케일로 나눠서 콘텐츠를 확대합니다 캡처된 콘텐츠의 픽셀 크기가 원본 창과 일대일로 일치하도록 크기가 조정됐습니다 그러나 타깃 디스플레이에서는 캡처된 창이 어떻게 보일까요? 그 질문에 대답하려면 가장 먼저 스케일 팩터의 원리를 알아야 합니다 디스플레이의 스케일 팩터란 디스플레이 또는 창에서 논리적 점 크기와 배경 표면 픽셀 크기의 스케일 비율을 나타냅니다 스케일이 2인 2x 모드는 화면의 모든 점 하나가 배경 표면의 픽셀 네 개와 일치한다는 의미입니다 이 예시처럼 캡처가 진행되는 중에도 스케일이 2인 Retina 디스플레이에서 스케일이 1인 비 Retina 디스플레이로 창을 옮길 수 있습니다 스케일이 1이면 화면의 논리적 점 하나가 배경 표면의 픽셀 하나에 해당합니다 또한 소스 디스플레이의 스케일이 캡처된 콘텐츠가 표시될 타깃 디스플레이의 스케일과 일치하지 않는 때도 있습니다 이 예시에서 왼쪽의 Retina 디스플레이가 스케일 팩터 2로 캡처되고 있습니다 오른쪽에 있는 비 Retina 디스플레이에 표시되죠 만약 캡처된 창이 스케일링 없이 그대로 비 Retina 디스플레이에 점 하나당 1픽셀로 매핑되면 창은 네 배 크게 표시될 것입니다 이 문제를 해결하려면 프레임의 메타데이터에서 스케일 팩터를 확인하고 타깃 디스플레이의 스케일 팩터와 비교하세요 스케일이 불일치하면 캡처된 콘텐츠를 표시하기 전에 스케일 팩터로 크기를 조정합니다 스케일을 조정하면 타깃 디스플레이의 캡처된 창은 소스 창과 동일한 크기로 나타날 겁니다 이제 코드를 살펴보겠습니다 아주 간단하죠 콘텐츠 사각형 콘텐츠 스케일, 스케일 팩터는 출력 CMSampleBuffer의 메타데이터 첨부에서도 찾아볼 수 있습니다 다음에는 이 메타데이터를 활용하여 캡처한 콘텐츠의 크기를 조정해 올바르게 표시할 수 있죠 요약하자면 단일 창 필터는 소스 창이 화면 밖에 있거나 가려지더라도 항상 전체 창 콘텐츠를 포함시킵니다 디스플레이와 공간에 독립적으로 작동하죠 출력은 언제나 왼쪽 상단 모서리에서 시작합니다 팝업 또는 자식 창은 포함시키지 않습니다 콘텐츠를 잘 표시하려면 메타데이터를 활용하세요 오디오는 창에 포함된 모든 앱의 트랙이 캡처됩니다 지금까지 단일 창을 캡처하고 표시하는 방법을 배웠으니 디스플레이 기반 콘텐츠 필터의 다음 단계로 넘어가겠습니다 다음 예시에서는 창 또는 앱으로 디스플레이 기반 필터를 만드는 법을 배우고 비디오와 오디오 필터링 규칙의 몇 가지 차이점을 보여 드리겠습니다 디스플레이 기반 포함 필터는 캡처하려는 콘텐츠가 있는 디스플레이를 지정할 수 있습니다 기본 설정에서는 아무 창도 캡처되지 않습니다 캡처하려는 콘텐츠를 창별로 선택할 수 있죠 이 예시에서는 Safari 창과 Keynote 창을 디스플레이 필터에 추가했습니다 비디오 출력은 디스플레이 공간에 배치된 두 개의 창만 포함되고 오디오 출력은 Keynote 및 Safari 앱의 모든 사운드트랙이 포함됩니다 이 샘플 코드는 캡처에 포함된 창으로 디스플레이 기반 필터를 만드는 방법을 보여 줍니다 SCShareableContent와 windowID로 디스플레이 기반 SCWindows 목록을 만드세요 그다음 주어진 디스플레이와 포함된 창 목록으로 디스플레이 기반 SCContentFilter를 만듭니다 데스크톱 독립 창과 동일한 방식으로 필터 및 설정을 작성하고 스트림을 시작할 수 있습니다 스트림이 실행되었으니 스트림의 출력을 살펴보겠습니다 필터는 두 개의 Safari 창과 메뉴 표시줄 및 배경 화면 창을 포함하도록 구성됐습니다
창이 화면 밖으로 나가면 스트림 출력에서 제외됩니다 새 Safari 창이 열렸지만 새 창은 스트림 출력에 표시되지 않습니다 새 창은 필터에 포함되지 않았기 때문이죠 자식 창과 팝업 창에도 같은 규칙이 적용됩니다 스트림 출력에 뜨지 않죠 여러분의 스트림 출력에 자식 창이 자동으로 포함되도록 하려면 디스플레이 기반 필터를 캡처할 앱에 적용하세요 이 예시에서는 Safari와 Keynote 앱에 모든 창의 오디오 및 비디오 출력과 두 앱의 사운드트랙이 출력에 포함되도록 필터를 추가했습니다 창 예외 필터는 여러분의 출력에서 특정 창을 제외하는 강력한 방법입니다 디스플레이와 해당 앱들을 필터로 지정하세요 예를 들어 단일 Safari 창을 출력에서 제거했습니다 ScreenCaptureKit은 앱 단계에서 오디오 캡처를 활성화하므로 단일 Safari 창에서 오디오를 제외하는 것은 모든 Safari 앱에서 오디오 트랙을 제거하는 것과 마찬가지입니다 스트림의 비디오 출력에는 여전히 Safari 창이 포함되어 있지만 Safari 앱의 사운드트랙은 모두 제거되고 Keynote의 사운드트랙만 오디오 출력에 포함됩니다 이 코드 예시에는 SCWindows 대신 SCRunningApplications 목록을 포함하도록 SCContentFilter를 변경합니다 추가로 제외하고 싶은 개별 창이 있다면 SCWindows 목록을 작성하고 제외할 창 목록과 함께 SCApplications 목록을 사용하여 SCContentFilter를 만드세요 이제 해당 앱을 지정한 후 새 창이나 자식 창을 만들면 스트림 출력이 어떻게 보이는지 살펴보겠습니다 이번에는 Safari 앱과 시스템 창이 필터에 추가됩니다 이제 스트림 출력에 새 Safari 창이 자동으로 포함되고 자식 및 팝업 창에도 같은 규칙이 적용됩니다 이 기능은 여러분이 작업을 연습하거나 팝업 또는 새 창 호출 등 전체 작업을 시연하려 할 때 매우 유용합니다 스트림 출력에 콘텐츠를 추가하는 방법을 몇 가지 예시를 통해 보여 드렸습니다 다음 예제에서는 스트림 출력에서 콘텐츠를 제거하는 법을 살펴보죠 지금부터 보여드릴 예시는 공유 중인 디스플레이의 미리 보기가 포함된 화상 회의 모방 테스트 앱입니다 테스트 앱은 미리 보기에 반복적으로 표시되기 때문에 이른바 ‘미러 홀 효과’가 일어납니다 전체 디스플레이를 공유할 때도 미러 홀 효과를 피하려고 앱이 자신의 창과 캡처 미리 보기 및 참가자 카메라 보기를 제거하는 것이 일반적입니다 알림 창 등 다른 시스템 UI를 숨기기도 하죠 ScreenCaptureKit이 제공하는 일련의 제외 기반 필터를 활용하면 디스플레이 캡처에서 콘텐츠를 빠르게 제거할 수 있습니다 기본적으로 제외 기반 디스플레이 필터는 지정된 디스플레이의 창을 모두 캡처합니다 개별 창이나 앱을 제외 필터에 추가하여 제거할 수 있습니다 예를 들어 콘텐츠 캡처 테스트 앱과 알림 센터를 제외 앱 목록에 추가할 수 있습니다 목록의 앱들을 제외하는 디스플레이 기반 필터를 만들려면 번들 ID를 일치시켜서 제외할 SCApplications를 검색하는 것으로 시작하세요 스트림 출력에 포함시키고 싶은 개별 창이 있다면 excepting SCWindows로 선택 목록을 만들 수 있습니다 그다음에는 지정된 디스플레이와 제외할 앱 목록 및 제외 창 목록을 사용하여 콘텐츠 필터를 만듭니다 결과물을 살펴보죠 미러 홀 문제를 일으키던 콘텐츠 캡처 테스트 앱과 알림 창이 모두 스트림 출력에서 제거됐습니다 앱에서 열린 새 창과 자식 창도 자동으로 모두 제외됩니다 제외된 앱에 오디오가 포함되어 있다면 오디오 출력에서 오디오가 제거됩니다 지금까지 단일 창을 캡처하는 방법과 필터에서 창을 추가하고 제거하는 법을 알아봤습니다 스트림 구성으로 넘어가죠 이제부터 보여 드릴 몇 가지 예시에서는 여러분이 구성할 수 있는 다양한 스트림 속성과 화면 캡처 및 스트리밍을 위해 스트림을 설정하는 방법 및 실시간 미리 보기로 창 선택기를 만드는 법을 살펴보겠습니다 구성 속성부터 시작해 보죠 일반적으로 구성할 수 있는 스트림 속성이 준비돼 있습니다 예를 들어 스트림 출력 크기와 소스 및 대상 직사각형 색 공간, 색 매트릭스 픽셀 형식 커서 포함 여부 및 프레임률 제어 등이죠 각 속성을 자세히 살펴보겠습니다 출력 크기부터 시작하죠 너비와 높이를 픽셀 단위로 지정할 수 있습니다 소스 디스플레이의 크기와 종횡비가 항상 출력 크기와 일치하진 않습니다 전체 디스플레이 캡처에서 크기가 일치하지 않으면 스트림 출력에 기둥처럼 레터박스가 생기죠 캡처할 영역을 정의하는 소스 사각형을 지정하면 그 결과 프레임 출력의 대상 사각형으로 렌더링과 스케일링이 이뤄집니다 ScreenCaptureKit은 하드웨어 가속 색 공간과 색 매트릭스 및 픽셀 형식 변환을 지원합니다 일반적인 BGRA와 YUV 형식을 지원합니다 개발자 페이지에서 전체 목록을 확인하세요 커서 표시가 활성화되면 스트림 출력에 프레임 안으로 미리 렌더링된 커서가 포함됩니다 이것은 모든 시스템 커서에 적용됩니다 카메라처럼 생긴 사용자 지정 커서도 포함되죠 최소 프레임 간격을 사용하면 출력 프레임률을 마음대로 제어할 수 있습니다 예를 들어 60fps를 요청하려면 최소 간격을 1/60로 설정하세요 60fps가 넘는 프레임 업데이트는 받지 않고 콘텐츠의 기본 프레임률도 넘기지 않습니다 대기열 깊이를 지정해서 서버 측 표면 풀의 표면 수를 결정할 수 있습니다 풀에 표면이 많을수록 프레임률과 성능이 향상되지만 대신 시스템 메모리 사용량이 높아집니다 지연 시간 손실이 생길 수도 있죠 이건 나중에 자세히 다루겠습니다 ScreenCaptureKit은 대기열 깊이를 3에서 8까지 허용합니다 기본 대기열 깊이는 3입니다 지금 보시는 예시에서는 표면 풀 구성이 ScreenCaptureKit에서 렌더링할 수 있는 네 개의 표면을 포함하고 있습니다 현재 활성화된 표면 1에 ScreenCaptureKit이 다음 프레임을 렌더링 중입니다 표면 1 렌더링이 완료되면 ScreenCaptureKit이 앱에 표면 1을 전달합니다 앱이 표면 1을 처리하고 유지하는 사이 ScreenCaptureKit은 표면 2를 렌더링합니다 앱에서 표면을 사용하고 있으므로 표면 1은 사용 불가로 표시됩니다 표면 2가 완료되어 앱으로 보내면 ScreenCaptureKit이 표면 3을 렌더링합니다 그러나 아직도 앱이 표면 1을 처리 중이면 처리 속도보다 빠르게 프레임이 제공되므로 프레임이 뒤처지기 시작하죠 표면 풀에 표면이 아주 많이 포함되어 있으면 새 표면이 계속 쌓이기 시작하므로 프레임을 삭제해서 속도를 따라가는 것도 고려해 봐야 합니다 이 경우 표면이 풀에 더 많을수록 높은 지연 시간이 발생할 수 있습니다 ScreenCaptureKit이 사용할 수 있는 풀의 표면 수는 대기열 깊이에서 앱이 보유하고 있는 표면 수를 뺀 것과 같습니다 이 예시에서는 표면 1과 2를 아직 앱이 보유하고 있습니다 표면 풀에는 두 개의 표면이 남아 있죠 표면 3이 완료돼서 앱으로 보내지면 풀에서 사용할 수 있는 표면은 표면 4만 남습니다 앱이 표면 1, 2, 3을 계속 들고 있으면 ScreenCaptureKit이 렌더링할 표면이 부족해지면서 프레임 손실과 결함이 나타나기 시작할 것입니다 프레임 손실을 피하려면 ScreenCaptureKit이 표면 4 다음 프레임 렌더링을 시작하기 전에 앱이 표면 1의 처리를 끝내고 내보내야 합니다 이제 앱이 표면 1을 내보내면 ScreenCaptureKit이 다시 사용할 수 있습니다 다시 말해 프레임 지연과 손실을 피하려면 앱이 두 가지 규칙을 따라야 합니다 프레임 지연을 방지하려면 프레임 하나를 MinimumFrameInterval 내에서 처리해야 합니다 프레임 손실을 피하려면 표면을 앱에서 풀로 다시 내보내는 시간이 MinimumFrameInterval과 QueueDepth에서 1을 뺀 수치의 곱보다 작아야 합니다 ScreenCaptureKit에 사용할 표면이 부족해지면 지연 상태에 들어가며 새 프레임을 놓칠 것입니다 여러분이 구성할 수 있는 다양한 속성을 살펴보셨으니 몇 가지 예제를 통해 화면 캡처 및 스트리밍을 위한 스트림 구성법을 알아보죠 영상과 게임, 애니메이션 등 일부 화면 콘텐츠는 지속적으로 업데이트되거나 높은 프레임률을 요구합니다 그러나 키노트 창처럼 정적인 문자를 포함한 다른 화면 콘텐츠들은 프레임률보다 높은 해상도가 우선이죠 콘텐츠의 유형과 네트워크 상태에 따라 스트림 구성을 실시간으로 조정할 수 있습니다 다음 코드 예시에서는 4K 60fps 게임을 스트리밍할 수 있게 캡처하는 방법을 알아보겠습니다 우선 스트림 출력 크기를 픽셀 크기에서 4K로 설정하세요 최소 프레임 간격을 1/60으로 설정해서 출력 프레임률을 60fps로 설정합니다 인코딩 및 스트리밍 포맷은 YUV420을 사용합니다 선택적 소스 사각형은 화면의 일부만 캡처하도록 설정합니다 배경 채우기 색은 검은색으로 바꾸고 프레임 출력에 커서를 포함시킵니다 프레임률과 성능 최적화를 위해 표면 대기열 깊이를 5로 설정합니다 마지막으로 출력 스트림에서 오디오를 활성화합니다 지금까지 예시에서 본 모든 스트림 설정은 스트림을 다시 만들지 않아도 즉석에서 동적으로 변경할 수 있습니다 예를 들어 출력 크기와 같은 속성을 실시간으로 조정하고 프레임률을 동적으로 변경하며 스트림 필터를 업데이트할 수 있죠 지금 보시는 예시는 출력 크기를 4K에서 720p로 전환하는 코드입니다 프레임률은 60fps에서 15fps로 떨어트렸습니다 updateConfiguration를 호출하면 스트림을 중단하지 않고 간단히 새 설정을 적용할 수 있습니다 마지막 예시에서는 실시간 미리 보기를 사용하여 창 선택기를 만드는 과정을 따라가 보겠습니다 지금 보시는 예시는 일반적인 창 선택기의 모양입니다 보통 화면 공유를 이용한 온라인 회의 앱은 사용자에게 공유할 창을 선택하는 옵션을 제공합니다 ScreenCaptureKit은 실시간 콘텐츠 업데이트로 썸네일 크기 스트림을 다수 생성하는 효율적이고 뛰어난 솔루션을 제공합니다 구현도 아주 간단하죠 ScreenCaptureKit으로 이런 창 선택기를 만들 때 무엇이 필요한지 살펴보겠습니다 선택기를 만들려면 앱에서 사용자가 선택할 수 있는 창마다 그 창에 대한 단일 창 필터를 작성하는 것부터 시작하세요 필터 유형은 데스크톱 독립 창을 사용합니다 다음으로는 스트림 설정에서 썸네일 크기, 5fps 및 화면 표시는 BGRA 픽셀 포맷 대기열 깊이는 기본으로 두고 커서와 오디오를 해제하세요 단일 창 필터와 스트림 구성을 사용해서 창마다 하나의 스트림을 생성합니다 작업을 코드로 수행하려면 우선 데스크톱과 시스템 창을 제외하고 SCShareableContent를 작성하세요 다음으로 해당되는 창마다 데스크톱 독립 창 유형으로 콘텐츠 필터를 만듭니다 다음은 스트림 설정으로 넘어갑니다 썸네일 크기를 적절히 선택하세요 이 예시에서는 가로 284, 세로 182입니다 그리고 최소 프레임 간격을 1/5로 놓습니다 화면 표시 픽셀 포맷은 BGRA로 놓고 오디오와 커서를 해제합니다 프리뷰에서는 필요 없으니까요 높은 업데이트 빈도가 불필요하기 때문에 대기열 깊이를 3으로 설정합니다 스트림 콘텐츠 필터와 설정이 생성되었으니 이제 스트림을 생성할 준비를 마쳤습니다 창마다 스트림을 하나씩 만들고 스트림마다 스트림 출력을 추가한 다음 스트림을 시작합니다 마지막으로 스트림 목록에 첨부하세요 앞서 살펴본 샘플 코드로 생성한 실시간 미리 보기가 있는 창 선택기입니다 모든 썸네일은 실시간으로 업데이트되며 단일 창 필터가 있는 개별 스트림의 지원을 받습니다 ScreenCaptureKit을 활용하면 실시간 미리 보기 선택기를 쉽게 만들 수 있으므로 시스템에 주는 부담을 줄이면서 여러 실시간 화면 콘텐츠를 동시에 캡처할 수 있습니다 이제 제 동료인 Drew에게 넘기겠습니다 OBS가 ScreenCaptureKit를 도입한 소식에 대해 신나는 발표를 준비했죠 고마워요, Meng 안녕하세요, 저는 Drew입니다 Apple의 파트너 엔지니어죠 OBS Studio는 사용자가 컴퓨터로 녹화 및 스트리밍 콘텐츠를 관리할 수 있는 오픈소스 앱입니다 이번 봄 통합 프로젝트와 함께 ScreenCaptureKit도 도입했죠 CGDisplayStream 캡처와 유사한 코드를 활용했기 때문에 ScreenCaptureKit의 도입은 어렵지 않았습니다 ScreenCaptureKit 도입은 'ScreenCaptureKit 소개' 세션에서 말씀드린 다양한 기능을 실현했습니다 여기에는 전체 데스크톱 캡처와 애플리케이션의 모든 앱 캡처 특정 단일 창 캡처가 포함되죠 ScreenCaptureKit은 OBS가 쓰는 CGWindowListCreateImage 캡처보다 오버헤드가 낮습니다 화면의 일부를 캡처할 때 콘텐츠 제작에 사용할 리소스가 더 많이 남는다는 뜻이죠 예시를 살펴보며 저희가 논의한 내용을 확인해 보겠습니다 왼쪽은 OBS에서 사용하는 Window Capture의 최악의 사례입니다 CGWindowListCreateImage API를 사용해서 화면이 심각하게 끊기죠 테스트에서는 프레임률이 7fps까지 떨어졌습니다 한편 ScreenCaptureKit을 도입한 오른쪽은 결과가 훨씬 부드럽고 출력 비디오에 더 부드러운 움직임을 제공합니다 여기서는 60fps죠 OBS의 램 사용량은 Window Capture보다 최대 15% 적습니다 Window Capture 대신 ScreenCaptureKit를 사용하면 CPU 사용률이 최대 절반까지 줄어듭니다 ScreenCaptureKit이 제공하는 개선 사항을 더 살펴보겠습니다 'Sayonara Wild Hearts'에서 골드 랭크에 도전하고 있습니다 제 최고의 플레이를 보여 주려고 게임 장면을 녹화했죠 ScreenCaptureKit 덕분에 게임에서 오디오 스트림을 직접 녹음합니다 Mac에서 알림이 울려도 제 오디오나 비디오 기록을 망치지 않죠 오디오 라우팅 소프트웨어를 추가로 설치하지 않아도 가능합니다 ♪ ScreenCaptureKit이 제공하는 개선점 덕분에 Mac에서 '태고의 달인'과 같은 게임을 인기 스트리밍 서비스로 방송할 수 있습니다 Apple CPU 하드웨어 인코더의 고정 비트 전송률 옵션으로 게임 성능에 큰 영향을 주지 않으면서 높은 비트 전송률을 요구하는 스트리밍이 가능해졌습니다 이제 ScreenCaptureKit의 낮은 자원 사용량과 인코딩 오프로딩 덕분에 중요한 콘텐츠에 더 많은 성능을 할애할 수 있습니다 넘길게요, Meng 고마워요, Drew 여러 시연과 예시를 통해 고급 화면 콘텐츠 필터를 알아봤습니다 사례에 따라 스트림을 구성하는 다양한 방법과 프레임별 메타데이터를 사용하고 캡처한 콘텐츠를 올바르게 표시하는 방법을 배웠죠 최고의 성과를 이끌어 낼 모범 사례도 살펴봤습니다 마지막으로 Drew가 ScreenCaptureKit이 OBS에 도입한 기능과 성능 개선을 소개했습니다 여러분이 ScreenCaptureKit을 활용해서 앱의 화면 공유와 스트리밍 및 협업 환경을 재정의하는 방법을 어서 보고 싶네요 시청해 주셔서 감사합니다 ♪
-
-
4:36 - Create a single window filter
// Get all available content to share via SCShareableContent let shareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false) // Get window you want to share from SCShareableContent guard let window : [SCWindow] = shareableContent.windows.first( where: { $0.windowID == windowID }) else { return } // Create SCContentFilter for Independent Window let contentFilter = SCContentFilter(desktopIndependentWindow: window) // Create SCStreamConfiguration object and enable audio capture let streamConfig = SCStreamConfiguration() streamConfig.capturesAudio = true // Create stream with config and filter stream = SCStream(filter: contentFilter, configuration: streamConfig, delegate: self) stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: serialQueue) stream.startCapture()
-
9:38 - Get dirty rects
// Get dirty rects from CMSampleBuffer dictionary metadata func streamUpdateHandler(_ stream: SCStream, sampleBuffer: CMSampleBuffer) { guard let attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo, Any]], let attachments = attachmentsArray.first else { return } let dirtyRects = attachments[.dirtyRects] } } // Only encode and transmit the content within dirty rects
-
13:34 - Get content rect, content scale and scale factor
/* Get and use contentRect, contentScale and scaleFactor (pixel density) to convert the captured window back to its native size and pixel density */ func streamUpdateHandler(_ stream: SCStream, sampleBuffer: CMSampleBuffer) { guard let attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo, Any]], let attachments = attachmentsArray.first else { return } let contentRect = attachments[.contentRect] let contentScale = attachments[.contentScale] let scaleFactor = attachments[.scaleFactor] /* Use contentRect to crop the frame, and then contentScale and scaleFactor to scale it */ } }
-
15:37 - Create display filter with included windows
// Get all available content to share via SCShareableContent let shareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false) // Create SCWindow list using SCShareableContent and the window IDs to capture let includingWindows = shareableContent.windows.filter { windowIDs.contains($0.windowID)} // Create SCContentFilter for Full Display Including Windows let contentFilter = SCContentFilter(display: display, including: includingWindows) // Create SCStreamConfiguration object and enable audio let streamConfig = SCStreamConfiguration() streamConfig.capturesAudio = true // Create stream stream = SCStream(filter: contentFilter, configuration: streamConfig, delegate: self) stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: serialQueue) stream.startCapture()
-
18:13 - Create display filter with included apps
// Get all available content to share via SCShareableContent let shareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false) /* Create list of SCRunningApplications using SCShareableContent and the application IDs you’d like to capture */ let includingApplications = shareableContent.applications.filter { appBundleIDs.contains($0.bundleIdentifier) } // Create SCWindow list using SCShareableContent and the window IDs to except let exceptingWindows = shareableContent.windows.filter { windowIDs.contains($0.windowID) } // Create SCContentFilter for Full Display Including Apps, Excepting Windows let contentFilter = SCContentFilter(display: display, including: includingApplications, exceptingWindows: exceptingWindows)
-
20:46 - Create display filter with excluded apps
// Get all available content to share via SCShareableContent let shareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false) /* Create list of SCRunningApplications using SCShareableContent and the app IDs you’d like to exclude */ let excludingApplications = shareableContent.applications.filter { appBundleIDs.contains($0.bundleIdentifier) } // Create SCWindow list using SCShareableContent and the window IDs to except let exceptingWindows = shareableContent.windows.filter { windowIDs.contains($0.windowID) } // Create SCContentFilter for Full Display Excluding Windows let contentFilter = SCContentFilter(display: display, excludingApplications: excludingApplications, exceptingWindows: exceptingWindows)
-
28:46 - Configure 4k 60FPS capture for streaming
let streamConfiguration = SCStreamConfiguration() // 4K output size streamConfiguration.width = 3840 streamConfiguration.height = 2160 // 60 FPS streamConfiguration.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(60)) // 420v output pixel format for encoding streamConfiguration.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange // Source rect(optional) streamConfiguration.sourceRect = CGRectMake(100, 200, 3940, 2360) // Set background fill color to black streamConfiguration.backgroundColor = CGColor.black // Include cursor in capture streamConfiguration.showsCursor = true // Valid queue depth is between 3 to 8 streamConfiguration.queueDepth = 5 // Include audio in capture streamConfiguration.capturesAudio = true
-
30:08 - Live downgrade 4k 60FPS to 720p 15FPS
// Update output dimension down to 720p streamConfiguration.width = 1280 streamConfiguration.height = 720 // 15FPS streamConfiguration.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(15)) // Update the configuration try await stream.updateConfiguration(streamConfiguration)
-
31:57 - Build a window picker with live preview
// Get all available content to share via SCShareableContent let shareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) // Create a SCContentFilter for each shareable SCWindows let contentFilters = shareableContent.windows.map { SCContentFilter(desktopIndependentWindow: $0) } // Stream configuration let streamConfiguration = SCStreamConfiguration() // 284x182 frame output streamConfiguration.width = 284 streamConfiguration.height = 182 // 5 FPS streamConfiguration.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(5)) // BGRA pixel format for on screen display streamConfiguration.pixelFormat = kCVPixelFormatType_32BGRA // No audio streamConfiguration.capturesAudio = false // Does not include cursor in capture streamConfiguration.showsCursor = false // Valid queue depth is between 3 to 8 // Create a SCStream with each SCContentFilter var streams: [SCStream] = [] for contentFilter in contentFilters { let stream = SCStream(filter: contentFilter, streamConfiguration: streamConfig, delegate: self) try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: serialQueue) try await stream.startCapture() streams.append(stream) }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.