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

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과 고유하게 연결되고, 다른 캡처 세션과 연결될 수 없으며, 객체가 정의된 페이지를 탐색한 후에도 유지되지 않습니다. 그러나 캡처 세션은 캡처된 페이지의 탐색 후에도 존속합니다.

사용자에게 권한 메시지를 표시하려면 사용자 동작이 필요합니다. 메시지를 표시해야 하는 경우에만 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 further explained 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팀과 웹 표준 커뮤니티는 Captured Surface Control 사용 경험에 관한 의견을 듣고자 합니다.

설계에 대해 알려주세요.

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

구현에 문제가 있나요?

Chrome 구현에서 버그를 발견하셨나요? 아니면 구현이 사양과 다른가요? https://new.crbug.com에서 버그를 신고합니다. 재현을 위한 안내와 함께 최대한 많은 세부정보를 제공해 주시기 바랍니다. Glitch는 재현 가능한 버그를 공유하는 데 효과적입니다.