HTML5에서 브라우저 탭을 화면공유하는 경우

지난 몇 년 동안 저는 브라우저 기술만 사용하여 화면 공유와 같은 기능을 구현하는 데 여러 회사를 도왔습니다. 경험에 비추어 볼 때 웹 플랫폼 기술 (즉, 플러그인 없음)만으로 VNC를 구현하는 것은 어려운 문제입니다. 고려해야 할 사항이 많고 극복해야 할 과제도 많습니다. 마우스 포인터 위치 전달, 키 입력 전달, 60fps에서 전체 24비트 색상 다시 칠하기 달성 등이 그중 일부입니다.

탭 콘텐츠 캡처

기존 화면 공유의 복잡성을 제거하고 브라우저 탭의 콘텐츠 공유에 집중하면 문제는 크게 단순화되어 a.) 현재 상태에서 표시된 탭을 캡처하고 b.) 이 '프레임'을 전송하는 것으로 귀결됩니다. 기본적으로 DOM을 스냅샷으로 찍고 공유할 방법이 필요합니다.

공유는 간단합니다. 웹소켓은 다양한 형식 (문자열, JSON, 바이너리)으로 데이터를 전송할 수 있습니다. 스냅샷 부분은 훨씬 더 어려운 문제입니다. html2canvas와 같은 프로젝트는 JavaScript로 브라우저의 렌더링 엔진을 다시 구현하여 HTML 스크린샷을 캡처했습니다. 오픈소스는 아니지만 Google 의견도 이러한 도구의 한 가지 예입니다. 이러한 유형의 프로젝트는 매우 멋지지만 속도가 매우 느립니다. 1fps 처리량을 얻는 것이 고작이며, 탐내는 60fps는 꿈도 꾸지 못합니다.

이 도움말에서는 탭을 '화면 공유'하는 데 사용할 수 있는 몇 가지 개념 증명 솔루션을 설명합니다.

방법 1: 변형 관찰자 + WebSocket

탭을 미러링하는 한 가지 방법은 올해 초 +라파엘 와인스타인이 시연했습니다. 이 기법에서는 변경 관찰자와 WebSocket을 사용합니다.

기본적으로 발표자가 공유하는 탭은 페이지 변경사항을 감시하고 웹소켓을 사용하여 뷰어에게 차이를 전송합니다. 사용자가 스크롤하거나 페이지와 상호작용할 때 관찰자는 이러한 변경사항을 선택하고 Rafael의 변형 요약 라이브러리를 사용하여 뷰어에게 다시 보고합니다. 이렇게 하면 성능이 유지됩니다. 모든 프레임에 전체 페이지가 전송되지는 않습니다.

동영상에서 라파엘이 지적했듯이 이는 단지 개념 증명일 뿐입니다. 그래도 Mutation Observer와 같은 최신 플랫폼 기능을 WebSockets와 같은 이전 기능과 결합하는 좋은 방법이라고 생각합니다.

방법 2: HTMLDocument의 Blob + 바이너리 WebSocket

다음 방법은 최근에 알게 된 방법입니다. 이는 변형 관찰자 접근 방식과 유사하지만 요약 대신 전체 HTMLDocument의 Blob 클론을 만들고 바이너리 웹소켓을 통해 전송합니다. 설정별 설정은 다음과 같습니다.

  1. 페이지의 모든 URL을 절대 URL로 다시 작성합니다. 이렇게 하면 정적 이미지 및 CSS 애셋에 잘못된 링크가 포함되지 않습니다.
  2. 페이지의 문서 요소를 클론합니다. document.documentElement.cloneNode(true);
  3. 클론을 읽기 전용으로 만들고 선택할 수 없게 하며 CSS pointer-events: 'none';user-select:'none';overflow:hidden;를 사용하여 스크롤을 방지합니다.
  4. 페이지의 현재 스크롤 위치를 캡처하여 중복 페이지에 data-* 속성으로 추가합니다.
  5. 중복의 .outerHTML에서 new Blob()를 만듭니다.

코드는 다음과 같습니다 (전체 소스를 단순화함).

function screenshotPage() {
    // 1. Rewrite current doc's imgs, css, and script URLs to be absolute before
    // we duplicate. This ensures no broken links when viewing the duplicate.
    urlsToAbsolute(document.images);
    urlsToAbsolute(document.querySelectorAll("link[rel='stylesheet']"));
    urlsToAbsolute(document.scripts);

    // 2. Duplicate entire document tree.
    var screenshot = document.documentElement.cloneNode(true);

    // 3. Screenshot should be readyonly, no scrolling, and no selections.
    screenshot.style.pointerEvents = 'none';
    screenshot.style.overflow = 'hidden';
    screenshot.style.userSelect = 'none'; // Note: need vendor prefixes

    // 4. … read on …

    // 5. Create a new .html file from the cloned content.
    var blob = new Blob([screenshot.outerHTML], {type: 'text/html'});

    // Open a popup to new file by creating a blob URL.
    window.open(window.URL.createObjectURL(blob));
}

urlsToAbsolute()에는 상대 URL/스킴 없는 URL을 절대 URL로 재작성하는 간단한 정규식이 포함되어 있습니다. 이는 blob URL (예: 다른 출처)의 컨텍스트에서 이미지, CSS, 글꼴, 스크립트를 볼 때 중단되지 않도록 하기 위해 필요합니다.

마지막으로 스크롤 지원을 추가했습니다. 발표자가 페이지를 스크롤하면 시청자도 따라야 합니다. 이렇게 하려면 현재 scrollXscrollY 위치를 중복 HTMLDocumentdata-* 속성으로 저장합니다. 최종 Blob이 생성되기 전에 페이지 로드 시 실행되는 JS가 삽입됩니다.

// 4. Preserve current x,y scroll position of this page. See addOnPageLoad().
screenshot.dataset.scrollX = window.scrollX;
screenshot.dataset.scrollY = window.scrollY;

// 4.5. When screenshot loads (e.g. in blob URL), scroll it to the same location
// of this page. Do this by appending a window.onDOMContentLoaded listener
// which pulls out the screenshot (dupe's) saved scrollX/Y state on the DOM.
var script = document.createElement('script');
script.textContent = '(' + addOnPageLoad_.toString() + ')();'; // self calling.
screenshot.querySelector('body').appendChild(script);

// NOTE: Not to be invoked directly. When the screenshot loads, scroll it
// to the same x,y location of original page.
function addOnPageLoad() {
    window.addEventListener('DOMContentLoaded', function(e) {
    var scrollX = document.documentElement.dataset.scrollX || 0;
    var scrollY = document.documentElement.dataset.scrollY || 0;
    window.scrollTo(scrollX, scrollY);
    });

스크롤을 조작하면 원래 페이지의 일부를 스크린샷한 것처럼 보이지만 실제로는 전체를 복제하여 위치만 변경한 것입니다. #clever

데모

하지만 탭 공유의 경우 탭을 계속 캡처하여 시청자에게 전송해야 합니다. 이를 위해 흐름을 보여주는 작은 Node websocket 서버, 앱, 북마크 래트를 작성했습니다. 코드에 관심이 없다면 다음의 짧은 동영상을 참고하세요.

향후 개선사항

한 가지 최적화 방법은 모든 프레임에서 전체 문서를 복제하지 않는 것입니다. 이는 낭비이며 Mutation Observer 예시에서 잘 처리됩니다. 또 다른 개선사항은 urlsToAbsolute()에서 상대 CSS 배경 이미지를 처리하는 것입니다. 이는 현재 스크립트에서 고려하지 않는 사항입니다.

방법 3: Chrome 확장 프로그램 API + 바이너리 WebSocket

Google I/O 2012에서는 브라우저 탭의 콘텐츠를 화면 공유하는 다른 접근 방식을 시연했습니다. 하지만 이 방법은 속임수입니다. 순수한 HTML5 매직이 아니라 Chrome Extension API가 필요합니다.

이 문제의 소스도 GitHub에 있지만 요약하면 다음과 같습니다.

  1. 현재 탭을 .png dataURL로 캡처합니다. Chrome 확장 프로그램에는 이 chrome.tabs.captureVisibleTab()에 관한 API가 있습니다.
  2. dataURL을 Blob로 변환합니다. convertDataURIToBlob() 도우미를 참고하세요.
  3. socket.responseType='blob'를 설정하여 바이너리 웹소켓을 사용하여 각 Blob (프레임)을 뷰어에게 전송합니다.

다음은 현재 탭의 스크린샷을 PNG로 캡처하고 웹소켓을 통해 프레임을 전송하는 코드입니다.

var IMG_MIMETYPE = 'images/jpeg'; // Update to image/webp when crbug.com/112957 is fixed.
var IMG_QUALITY = 80; // [0-100]
var SEND_INTERVAL = 250; // ms

var ws = new WebSocket('ws://…', 'dumby-protocol');
ws.binaryType = 'blob';

function captureAndSendTab() {
    var opts = {format: IMG_MIMETYPE, quality: IMG_QUALITY};
    chrome.tabs.captureVisibleTab(null, opts, function(dataUrl) {
    // captureVisibleTab returns a dataURL. Decode it -> convert to blob -> send.
    ws.send(convertDataURIToBlob(dataUrl, IMG_MIMETYPE));
    });
}

var intervalId = setInterval(function() {
    if (ws.bufferedAmount == 0) {
    captureAndSendTab();
    }
}, SEND_INTERVAL);

향후 개선사항

프레임 속도는 놀라울 정도로 좋지만 더 나을 수 있습니다. dataURL을 Blob으로 변환하는 오버헤드를 제거하는 것이 하나의 개선 방법입니다. 안타깝게도 chrome.tabs.captureVisibleTab()는 dataURL만 제공합니다. Blob 또는 유형 배열을 반환하면 Blob으로 직접 변환하는 대신 웹소켓을 통해 직접 전송할 수 있습니다. 이를 위해 crbug.com/32498에 별표표시를 해 주세요.

방법 4: WebRTC - 진정한 미래

마지막으로 중요한 점은

브라우저에서의 화면 공유의 미래는 WebRTC로 실현됩니다. 2012년 8월 14일에 팀은 탭 콘텐츠 공유를 위한 WebRTC 탭 콘텐츠 캡처 API를 제안했습니다.

이 메서드가 준비될 때까지는 1~3번 메서드를 사용해야 합니다.

결론

따라서 오늘날의 웹 기술을 사용하면 브라우저 탭을 공유할 수 있습니다.

하지만 이 말은 약간 과장된 표현입니다. 이 도움말의 기법은 깔끔하지만, 어쨌든 우수한 공유 UX에는 미치지 못합니다. WebRTC 탭 콘텐츠 캡처 작업으로 이 모든 것이 바뀔 예정이지만, 그때까지는 브라우저 플러그인이나 여기에서 다룬 것과 같은 제한된 솔루션을 사용해야 합니다.

다른 기법이 더 있나요? 댓글을 남겨주세요.