WebRTC - 실시간 PC 화면 가져오기


WebRTC는 웹을 통해 유저 간의 실시간 통신을 가능케 하는 기술이다. 물론 WebRTC 기술을 사용하지 않고도 이미 많은 서비스가 실시간 통신을 지원하고 있다. 하지만 비싼 영상/음성 기술을 사용해야 하고, 사용자는 네이티브 앱이나 플러그인을 설치해야 하는 등의 장벽이 존재한다. 그렇지만 WebRTC는 플러그인이 필요 없는, 실시간의, 영상과 음성, 데이터 통신에 대한 표준과 기술을 제공한다. 영상, 음성, 메시지, 파일까지 중개 서버가 필요 없는 피어간 통신(P2P)을 지원한다.

WebRTC Tutorial

조금 더 자세한 WebRTC 소개와 튜토리얼은 아래 페이지들을 참고하면 좋다.

Screen sharing

WebRTC를 이용하여 기본적인 화상/음성/메시지/데이터 통신을 할 수 있다. 그리고 심지어 자신의 컴퓨터 화면마저도 브라우저를 통해 상대방과 P2P로 공유하고 통신할 수 있다. 그렇지만 여기에는 한가지 제약조건이 있는데, 브라우저에서 로컬 피씨의 화면을 가져오기 위해서는 영상이나 음성과는 달리 브라우저의 Extension App을 이용해야 하는 것이다. (현재는 Firefox, Chrome 정도의 브라우저만이 Extension API와 App 개발 플랫폼을 제공하고 있다.) 그 이유는 보안과 관련되어 있는데, 자세한 히스토리는 WebRTC그룹의 글을 참고하면 알 수 있다.

WebRTC를 이용한 Screen sharing은 다음과 같은 Flow를 갖는다.
image


이 글은 WebRTC를 통해 피씨의 Screen Sharing을 하기 위해 선행되어야 하는 기술들과 API, 구조를 크롬의 chrome.desktopCapture Extension API 기준으로 설명한다. 사실 Screen Sharing은 PC 화면의 Stream만 얻어 올 수 있다면, 그다음은 WebRTC의 일반적인 (비디오 캠과 같은)영상을 실시간으로 통신하게 하는 방식과 동일하게 구현할 수 있다. 유저 간 실시간 통신을 위한 Signaling 과정이나 PeerConnection은 위의 WebRTC Tutorial에 있는 링크를 참고한다.

Extension App

앞서 설명한 Screen Sharing에는 Extension App이 필요하였다. 이 Extension App에 대해 조금 더 정확히 설명하자면, 사실 App 자체로 화면 Stream을 얻고 구현하는 것은 아니다. 단순히 Extension API를 호출하여 로컬 피씨의 Screen Stream Id를 가져오고 실제 웹 페이지와 통신하여 이 Id를 전달하는 역할이 전부다.

웹 페이지는 Extension App으로부터 전달받은 Id를 가지고 getUserMedia API를 호출하여 실제 Stream 객체를 얻어낸다.

Stream Id (chromeMediaSourceId)

An opaque string that can be passed to getUserMedia() API to generate media stream that corresponds to the source selected by the user. If user didn't select any source (i.e. canceled the prompt) then the callback is called with an empty streamId. The created streamId can be used only once and expires after a few seconds when it is not used.

일단 그럼 로컬 PC의 화면을 얻어오기 위해서는 Extension App을 만들어야 한다. 개발 언어 및 구조는 Javascript + JSON + HTML + CSS 조합이므로 만드는데 크게 어렵지 않다. 크롬의 개발 가이드 문서Extension samples를 참고하도록 하자.

Extension API - chrome.desktopCapture

chrome.desktopCapture.chooseDesktopMedia API는 Chrome 34버전부터 사용할 수 있다. 이 Extension API를 통해 로컬 PC의 Screen Stream Id를 얻어올 수 있다. 이 API는 복잡하거나 어렵지 않고, 사용 예제도 Extension samples 페이지에서 Desktop Capture로 검색하면 찾을 수 있다.

Screen Sharing을 위한 Extension App을 개발한다면 아마 보통은 다음 4개의 파일로 구성될 것이다.

  1. background.js
  2. 실제 Extension API - chrome.desktopCapture를 사용하는 파일이다.
  3. content-script.js
  4. Extension App을 통해 웹 페이지에 주입(Injection)하는 파일이다. 이 파일을 통해 Extension App과 웹 페이지 간 통신을 할 수 있다.
  5. manifest.json
  6. Extension App의 환경 설정 파일이다. 이름, 설명, 버전, 권한 등의 내용이 포함되어 있다. 자세한 key-value 포맷은 여기를 참고한다.
  7. desktopCapture API를 이용하기 위해서는 "desktopCapture" 권한을 필수로 추가해야 한다.
    "permissions": ["desktopCapture", ...]
  8. icon.png
  9. 중요한 파일은 아니다. 그냥 Extension Icon 이미지 파일이다. Extension App이 설치되었는지 확인하기 위해 Image-ping방식으로 쓰이기도 한다.

웹 페이지와 Extenion App의 통신

웹 페이지와 Extension App이 통신하는 구조는 다음과 같다.
image

Background Script

  1. Content Script를 주입한다. 주입하는 방식은 아래 두가지가 있다.
  2. manifest에 등록하여 주입
  3. chrome.tabs.executeScript API를 통해 주입
  4. Content Script와 통신할 수 있도록 port를 통한 메시지 송/수신을 준비한다.
// 메시지는 개발자 임의로 정의하여 사용한다.
chrome.runtime.onConnect.addListener(port => {
  port.onMessage.addListener(msg => {
    if (msg.type === "REQUEST_SCREEN_STREAM_ID") {
      requestScreenStreamId(port, msg);
    }
    // ...
  });
});
  1. 메시지가 수신되면 chrome.desktopCapture.chooseDesktopMedia API를 호출한다.
function requestScreenStreamId(port, msg) {
  const sendMessage = {};
  const tab = port.sender.tab;
  tab.url = msg.url;

  chrome.desktopCapture.chooseDesktopMedia(
    ["screen", "window", "tab"],
    tab,
    streamId => {
      if (streamId) {
        sendMessage.streamId = streamId;
        //...
      } else {
        // Stream Id를 가져오는데 실패한 경우
        //...
      }
    }
  );

  port.postMessage(sendMessage);
}

Content Script

  1. Background Script와 통신할 수 있도록 port를 통한 메시지 송/수신을 준비한다.
    Background Script => Content Script => 웹 페이지
const port = chrome.runtime.connect(chrome.runtime.id);
port.onMessage.addListener(msg => {
  window.postMessage(msg, "*"); // Background Script에서 받은 메시지를 웹 페이지에 전달
});
  1. 웹 페이지와는 window.postMessage API를 통해 통신한다. (웹 페이지와 Content Script는 DOM을 공유하기 때문에 window.postMessage를 이용한다.)
    웹 페이지 => Content Script => BackgroundScript
window.addEventListener("message", event => {
  const type = event.data.type;
  if (type === "REQUEST_SCREEN_STREAM_ID") {
    port.postMessage(event.data); // 웹 페이지에서 받은 메시지를 Backgournd Script에 전달
  }
});

웹 페이지 Script

웹 페이지는 Content Script에게 Screen Stream Id를 받으면 getUserMedia API를 통해 실제 Stream 객체를 가져오고, 이 stream 객체를 상대 피어에게 전달하거나, 현재 페이지에 그대로 보여줄 수 있다.

const gotStream = screenStream => {
  const videoElement = document.getElementById("video");
  videoElement.src = URL.createObjectURL(screenStream);
  videoElement.play();
};

const onFail = err => {
  console.log(err);
};

window.addEventListener("message", event => {
  const streamId = event.data.streamId;

  if (streamId) {
    navigator.mediaDevices
      .getUserMedia({
        audio: false, // or true
        video: {
          mandatory: {
            chromeMediaSourceId: streamId,
            chromeMediaSource: "desktop",
            maxWidth: window.screen.width,
            maxHeight: window.screen.height
            //...
          }
        }
      })
      .then(gotStream)
      .catch(onFail);
  } else {
    //... stream Id 가져오기 실패
  }
});

전체 흐름

image

정상적으로 Extension App과 웹 페이지가 통신한다면 다음과 같은 화면 선택 창이 나타날 것이다.
image

외부 Extension App 활용하기

아무리 Extension App을 간단하게 개발할 수 있을지라도, 실제로 개발하고 Chrome Extension에서 로드하여 테스트하기는 솔직히 많이 귀찮은 작업이다. 때문에 단순한 테스트를 위해서라면 외부 Extension App과 통신하여 개발할 수도 있다. 가장 쉬운 예제는 아마 Muaz-khan의 getScreenId.js일 것이다.

  1. 우선 로컬 피씨에 Muaz-khan의 Extension App을 설치한다.
  2. 개발 페이지에 아래 스크립트 중 하나를 로드한다. (둘 다 큰 차이는 없다.)
<script src="https://cdn.WebRTC-Experiment.com/getScreenId.js"></script>

<!-- or -->
<script src="https://cdn.rawgit.com/muaz-khan/getScreenId/master/getScreenId.js"></script>
  1. 그리고 이제 window.getScreenId 함수를 호출한다.
getScreenId((error, sourceId, screenConstraints) => {
  if (error === "not-installed") return alert("The extension is not installed");
  if (error === "permission-denied") return alert("Permission is denied.");
  if (error === "not-chrome") return alert("Please use chrome.");

  navigator.mediaDevices
    .getUserMedia(screenConstraints)
    .then(stream => {
      const video = document.querySelector("video");
      video.src = URL.createObjectURL(stream);
      video.autoplay = true;
      video.controls = true;
      video.play();
    })
    .catch(err => {
      console.log(err);
    });
});
  1. 이제 손쉽게 로컬 PC 화면의 스트림을 가져와서 마음대로 테스트할 수 있다.
    image

참고

앞에서 설명하지 않았지만, WebRTC의 스펙과 API들은 아직 WorkingDraft 상태이기 때문에, 브라우저마다 구현이나 API명의 차이가 있다.
image

https://webrtc.org/web-apis/interop/

그래서 이런 차이를 맞춰주는 webrtc-adapter 라이브러리를 같이 사용하길 추천한다.

WebRTC의 여러 Example은 아래 페이지들에서 확인할 수 있다.


이민규, FE Development Lab2016.08.12Back to list