캡처된 탭 스크롤 및 확대/축소

François Beaufort
François Beaufort

Screen Capture API를 사용하면 웹 플랫폼에서 탭, 창, 화면을 이미 공유할 수 있습니다. 웹 앱이 getDisplayMedia()를 호출하면 Chrome에서 사용자에게 탭, 창 또는 화면을 웹 앱과 MediaStreamTrack 동영상으로 공유하라는 메시지를 표시합니다.

getDisplayMedia()를 사용하는 많은 웹 앱은 캡처된 노출 영역의 동영상 미리보기를 사용자에게 표시합니다. 예를 들어 화상 회의 앱은 이 동영상을 원격 사용자에게 스트리밍하는 동시에 로컬 HTMLVideoElement에 렌더링하여 로컬 사용자가 공유하는 항목의 미리보기를 계속 볼 수 있도록 합니다.

이 문서에서는 웹 앱이 캡처된 탭을 스크롤하고 캡처된 탭의 확대/축소 수준을 읽고 쓸 수 있는 Chrome의 새로운 Captured Surface Control API를 소개합니다.

사용자가 캡처된 탭을 스크롤하고 확대/축소합니다 (데모).

캡처된 노출 영역 컨트롤을 사용해야 하는 이유

모든 화상 회의 앱에는 동일한 단점이 있습니다. 사용자가 캡처된 탭이나 창과 상호작용하려면 해당 노출 영역으로 전환해야 하므로 화상 회의 앱에서 벗어나게 됩니다. 이로 인해 몇 가지 문제가 발생합니다.

  • 사용자가 PIP를 사용하거나 화상 회의 탭과 공유 탭을 위해 별도의 나란히 창을 열지 않는 한 캡처된 앱과 원격 사용자의 동영상을 동시에 볼 수 없습니다. 작은 화면에서는 이 작업이 어려울 수 있습니다.
  • 사용자는 화상 회의 앱과 캡처된 노출 영역 간에 전환해야 하는 부담을 느낍니다.
  • 사용자가 화상 회의 앱을 사용하지 않는 동안 앱에서 노출하는 컨트롤(예: 삽입된 채팅 앱, 그림 이모티콘 반응, 통화에 참여하도록 요청하는 사용자에 관한 알림, 멀티미디어 및 레이아웃 컨트롤, 기타 유용한 화상 회의 기능)에 액세스할 수 없습니다.
  • 발표자는 원격 참여자에게 제어 권한을 위임할 수 없습니다. 이로 인해 원격 사용자가 발표자에게 슬라이드를 변경하거나, 위아래로 약간 스크롤하거나, 확대/축소 수준을 조정하라고 요청하는 익숙한 상황이 발생합니다.

Captured Surface Control API는 이러한 문제를 해결합니다.

캡처된 노출 영역 컨트롤은 어떻게 사용하나요?

캡처된 노출 영역 컨트롤을 성공적으로 사용하려면 브라우저 탭을 명시적으로 캡처하고 사용자의 권한을 얻는 등의 몇 가지 단계를 거쳐야 캡처된 탭을 스크롤하고 확대/축소할 수 있습니다.

브라우저 탭 캡처

먼저 사용자에게 getDisplayMedia()를 사용하여 공유할 노출 영역을 선택하라는 메시지를 표시하고 이 과정에서 CaptureController 객체를 캡처 세션과 연결합니다. 곧 이 객체를 사용하여 캡처된 노출 영역을 제어할 예정입니다.

const controller = new CaptureController();
const stream = await navigator.mediaDevices.getDisplayMedia({ controller });

다음으로 캡처된 노출 영역의 로컬 미리보기를 <video> 요소 형식으로 생성합니다.

const previewTile = document.querySelector('video');
previewTile.srcObject = stream;

사용자가 창이나 화면을 공유하려고 하는 경우 지금은 지원 범위가 아닙니다. 하지만 탭을 공유하려는 경우 계속 진행할 수 있습니다.

const [track] = stream.getVideoTracks();

if (track.getSettings().displaySurface !== 'browser') {
  // Bail out early if the user didn't pick a tab.
  return;
}

권한 메시지

지정된 CaptureController 객체에서 sendWheel() 또는 setZoomLevel()를 처음 호출하면 권한 메시지가 표시됩니다. 사용자가 권한을 부여하면 해당 CaptureController 객체에서 이러한 메서드를 계속 호출할 수 있습니다. 사용자가 권한을 거부하면 반환된 프로미스가 거부됩니다.

CaptureController 객체는 특정 capture-session과 고유하게 연결되며 다른 capture-session과 연결할 수 없으며 정의된 페이지의 탐색 후에도 유지되지 않습니다. 하지만 캡처 세션은 캡처된 페이지의 탐색 후에도 유지됩니다.

사용자에게 권한 메시지를 표시하려면 사용자 동작이 필요합니다. 프롬프트를 표시해야 하는 경우에만 sendWheel()setZoomLevel() 호출에 사용자 동작이 필요합니다. 사용자가 웹 앱에서 확대 또는 축소 버튼을 클릭하면 해당 사용자 동작이 주어집니다. 하지만 앱에서 먼저 스크롤 컨트롤을 제공하려는 경우 개발자는 스크롤이 사용자 동작을 구성하지 않는다는 점에 유의해야 합니다. 한 가지 방법은 다음 예와 같이 먼저 사용자에게 '스크롤 시작' 버튼을 제공하는 것입니다.

const startScrollingButton = document.querySelector('button');

startScrollingButton.addEventListener('click', async () => {
  try {
    const noOpWheelAction = {};

    await controller.sendWheel(noOpWheelAction);
    // The user approved the permission prompt.
    // You can now scroll and zoom the captured tab as shown later in the article.
  } catch (error) {
    return; // Permission denied. Bail.
  }
});

스크롤

sendWheel()를 사용하면 캡처 앱이 탭의 뷰포트 내에서 선택한 좌표를 통해 선택한 크기의 휠 이벤트를 전송할 수 있습니다. 이 이벤트는 캡처된 앱에서 직접적인 사용자 상호작용과 구분할 수 없습니다.

캡처 앱이 "previewTile"라는 <video> 요소를 사용한다고 가정하면 다음 코드는 캡처된 탭에 휠 이벤트 전송을 릴레이하는 방법을 보여줍니다.

const previewTile = document.querySelector('video');

previewTile.addEventListener('wheel', async (event) => {
  // Translate the offsets into coordinates which sendWheel() can understand.
  // The implementation of this translation is explained further below.
  const [x, y] = translateCoordinates(event.offsetX, event.offsetY);
  const [wheelDeltaX, wheelDeltaY] = [-event.deltaX, -event.deltaY];

  try {
    // Relay the user's action to the captured tab.
    await controller.sendWheel({ x, y, wheelDeltaX, wheelDeltaY });
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

sendWheel() 메서드는 두 개의 값 집합이 있는 사전을 사용합니다.

  • xy: 휠 이벤트가 전송될 좌표입니다.
  • wheelDeltaXwheelDeltaY: 각각 가로 스크롤과 세로 스크롤의 스크롤 크기(픽셀)입니다. 이 값은 원래 휠 이벤트에 비해 반전됩니다.

translateCoordinates()의 가능한 구현은 다음과 같습니다.

function translateCoordinates(offsetX, offsetY) {
  const previewDimensions = previewTile.getBoundingClientRect();
  const trackSettings = previewTile.srcObject.getVideoTracks()[0].getSettings();

  const x = trackSettings.width * offsetX / previewDimensions.width;
  const y = trackSettings.height * offsetY / previewDimensions.height;

  return [Math.floor(x), Math.floor(y)];
}

위의 코드에는 세 가지 크기가 사용됩니다.

  • <video> 요소의 크기입니다.
  • 캡처된 프레임의 크기입니다 (여기서는 trackSettings.widthtrackSettings.height로 표시됨).
  • 탭의 크기입니다.

<video> 요소의 크기는 캡처 앱의 도메인 내에 완전히 있으며 브라우저에는 알 수 없습니다. 탭의 크기는 브라우저의 도메인 내에 완전히 있으며 웹 앱에는 알 수 없습니다.

웹 앱은 translateCoordinates()를 사용하여 <video> 요소를 기준으로 한 오프셋을 동영상 트랙 자체의 좌표 공간 내 좌표로 변환합니다. 브라우저는 캡처된 프레임의 크기와 탭의 크기 간에 변환하고 웹 앱의 기대에 상응하는 오프셋으로 스크롤 이벤트를 전송합니다.

sendWheel()가 반환하는 프로미스는 다음과 같은 경우에 거부될 수 있습니다.

  • 캡처 세션이 아직 시작되지 않았거나 이미 중지된 경우(예: 브라우저에서 sendWheel() 작업을 처리하는 동안 비동기식으로 중지됨)
  • 사용자가 앱에 sendWheel() 사용 권한을 부여하지 않은 경우
  • 캡처 앱이 [trackSettings.width, trackSettings.height] 외부의 좌표에서 스크롤 이벤트를 전송하려고 시도하는 경우 이러한 값은 비동기식으로 변경될 수 있으므로 오류를 포착하여 무시하는 것이 좋습니다. (0, 0는 일반적으로 범위를 벗어나지 않으므로 사용자에게 권한을 요청하는 메시지를 표시하는 데 이를 사용해도 안전합니다.)

확대/축소

캡처된 탭의 확대/축소 수준과 상호작용하는 작업은 다음 CaptureController 노출 영역을 통해 이루어집니다.

  • getSupportedZoomLevels()는 브라우저에서 지원하는 확대/축소 수준 목록을 반환하며, 이 목록은 100%로 정의된 '기본 확대/축소 수준'의 비율로 표시됩니다. 이 목록은 단조 증가하며 값 100을 포함합니다.
  • getZoomLevel()는 탭의 현재 확대/축소 수준을 반환합니다.
  • setZoomLevel()는 탭의 확대/축소 수준을 getSupportedZoomLevels()에 있는 정수 값으로 설정하고 성공하면 약속을 반환합니다. 캡처 세션이 끝나도 확대/축소 수준은 재설정되지 않습니다.
  • oncapturedzoomlevelchange를 사용하면 캡처 앱을 통해 또는 캡처된 탭과의 직접 상호작용을 통해 사용자가 확대/축소 수준을 변경할 수 있으므로 캡처된 탭의 확대/축소 수준 변경사항을 수신 대기할 수 있습니다.

setZoomLevel() 호출은 권한에 따라 제한됩니다. 다른 읽기 전용 확대/축소 메서드 호출은 이벤트 리슨과 마찬가지로 '무료'입니다.

다음 예는 기존 캡처 세션에서 캡처된 탭의 확대/축소 수준을 높이는 방법을 보여줍니다.

const zoomIncreaseButton = document.getElementById('zoomInButton');

zoomIncreaseButton.addEventListener('click', async (event) => {
  const levels = CaptureController.getSupportedZoomLevels();
  const index = levels.indexOf(controller.getZoomLevel());
  const newZoomLevel = levels[Math.min(index + 1, levels.length - 1)];

  try {
    await controller.setZoomLevel(newZoomLevel);
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

다음 예는 캡처된 탭의 확대/축소 수준 변경에 반응하는 방법을 보여줍니다.

controller.addEventListener('capturedzoomlevelchange', (event) => {
  const zoomLevel = controller.getZoomLevel();
  document.querySelector('#zoomLevelLabel').textContent = `${zoomLevel}%`;
});

기능 감지

휠 이벤트 전송이 지원되는지 확인하려면 다음을 사용하세요.

if (!!window.CaptureController?.prototype.sendWheel) {
  // CaptureController sendWheel() is supported.
}

확대/축소 제어가 지원되는지 확인하려면 다음을 사용하세요.

if (!!window.CaptureController?.prototype.setZoomLevel) {
  // CaptureController setZoomLevel() is supported.
}

캡처된 노출 영역 제어 사용 설정

Captured Surface Control API는 데스크톱용 Chrome에서 Captured Surface Control 플래그 뒤에 제공되며 chrome://flags/#captured-surface-control에서 사용 설정할 수 있습니다.

또한 이 기능은 데스크톱용 Chrome 122부터 시작되는 오리진 체험판에 포함되어 개발자가 사이트 방문자가 실제 사용자로부터 데이터를 수집할 수 있도록 이 기능을 사용 설정할 수 있습니다. 출처 무료 체험판 및 작동 방식에 대한 자세한 내용은 출처 무료 체험판 시작하기를 참고하세요.

보안 및 개인 정보 보호

"captured-surface-control" 권한 정책을 사용하면 캡처 앱과 삽입된 서드 파티 iframe이 캡처된 노출 영역 제어에 액세스하는 방식을 관리할 수 있습니다. 보안 절충점을 이해하려면 캡처된 노출 영역 제어 설명의 개인 정보 보호 및 보안 고려사항 섹션을 확인하세요.

데모

Glitch에서 데모를 실행하여 캡처된 노출 영역 컨트롤을 사용해 볼 수 있습니다. 소스 코드를 확인해야 합니다.

이전 버전의 Chrome에서 변경된 사항

다음은 캡처된 노출 영역 컨트롤에 관한 몇 가지 주요 동작 차이점입니다.

  • Chrome 124 이하:
    • 권한이 부여된 경우 이 권한의 범위는 캡처 출처가 아닌 해당 CaptureController와 연결된 캡처 세션으로 제한됩니다.
  • Chrome 122의 경우
    • getZoomLevel()는 탭의 현재 확대/축소 수준이 포함된 약속을 반환합니다.
    • 사용자가 앱 사용 권한을 부여하지 않은 경우 sendWheel()"No permission." 오류 메시지와 함께 거부된 프라미스를 반환합니다. Chrome 123 이상에서는 오류 유형이 "NotAllowedError"입니다.
    • oncapturedzoomlevelchange을(를) 사용할 수 없습니다. setInterval()를 사용하여 이 기능을 폴리필할 수 있습니다.

의견

Chrome팀과 웹 표준 커뮤니티는 캡처된 노출 영역 컨트롤 사용 경험에 관한 의견을 듣고자 합니다.

디자인에 관해 알려주세요.

캡처된 노출 영역 캡처가 예상대로 작동하지 않는 문제가 있나요? 아니면 아이디어를 구현하는 데 필요한 메서드나 속성이 누락되어 있나요? 보안 모델에 관해 질문이나 의견이 있으신가요? GitHub 저장소에서 사양 문제를 제출하거나 기존 문제에 의견을 추가하세요.

구현에 문제가 있나요?

Chrome 구현에서 버그를 발견했나요? 아니면 구현이 사양과 다른가요? https://new.crbug.com에서 버그를 신고합니다. 재현 안내와 함께 최대한 자세한 정보를 포함해야 합니다. Glitch는 재현 가능한 버그를 공유하는 데 적합합니다.