Getting Started with WebRTC

HTML5 Rocks

플러그인 없는 실시간 통신

WebRTC는 오픈 웹을 위한 기나긴 전쟁에 새로운 선두입니다.
Brendan Eich, 자바스크립트 창시자

전화기, TV, 컴퓨터가 공통 플랫폼위에서 모두 대화할 수 있는 세상을 상상해보십시요. 여러분의 웹어플리케이션에 비디오채팅 기능과 P2P 데이터 공유 기능을 쉽게 추가했다고 상상해보십시요. 이것이 WebRTC의 비전입니다.

확인해보고 싶으십니까? WebRTC는 현재 구글 Chrome과 Opera, Firefox에서 가능합니다. apprtc.appspot.com은 간단한 비디오 채팅 어플리케이션을 시작해볼수 있는 좋은 곳입니다:

  1. apprtc.appspot.com 페이지를 Chrome이나 Opera, Firefox에서 엽니다.
  2. 페이지(웹어플리케이션)에서 Webcam을 사용하도록 허용버튼을 클릭합니다.
  3. 다른 탭 또는 더 좋은 방법으로 다른 컴퓨터에서 페이지 하단에 표시된 URL을 브라우저에서 엽니다.

이 어플리케이션에 대한 설명은 본문 아래에서 다룹니다.

Quick start

본문을 읽기에 시간이 없거나 바로 코드만 보고 싶으십니까?

  1. Google I/O에서 발표되었던 WebRTC에 대한 소개를 시청하시기 바랍니다. (슬라이드 자료는 여기):

  2. getUserMedia를 이전에 사용해보지 않았다면, HTML5 Rocks 기사를 읽어 보시기 바랍니다. 그리고 simpl.info/gum에서 간단한 예제 코드를 확인할 수 있습니다.
  3. 뒤에 나오는 간단한 예제와 WebRTC를 single page로 구현한 데모를 통해서 RTCPeerConnection API를 파악하시기 바랍니다.
  4. apprtc.appspot.com에서 코드와 콘솔로그를 통해 WebRTC가 시그널링, 방화벽, NAT traversal을 위해 서버를 어떻게 사용하는지 더 깊이 공부해보시기 바랍니다.

아니면 저희의 WebRTC codelab으로 바로 들어오시기 바랍니다: 간단한 시그널링 서버를 포함한 비디오 채팅앱을 만드는 법을 step-by-step으로 설명한 가이드입니다.

WebRTC의 아주 짧은 역사

웹에서 남은 주요 도전 과제 중에 남은 하나는 영상과 음성을 통한 인간의 의사소통 - 실시간 대화(Real Time Communication, RTC) 가능하게 하는 것 입니다. 실시간 대화는 문자 입력란에 문자를 입력하는 것처럼 웹 어플리케이션에서 자연스러워야 하는 것입니다. 그렇지 않다면 사람들과 소통하는 새로운 방식들을 혁신하고 개발하는 능력을 우리는 한정 지어버리게 됩니다.

역사적으로 실시간 대화는 집에서 사용하기에 어려웠고, 비싼 음성/영상 기술들이 필요했습니다. 이미 존재하는 콘텐츠와 데이터 서비스들과 함께 실시간 대화 기술을 융합하는 것은 매우 어렵고 시간이 많이 드는 일이었습니다. 특히 웹에서.

Gmail의 영상채팅은 2008년에 출시되었고, 2011년에는 Google Talk 서비스를 (Gamil에서 처럼)사용한 행아웃이 소개되었습니다. 구글은 코덱, Echo cancellation 등의 실시간 통신에 필요한 많은 기술을 개발한 회사인 GIPS를 삽니다. 구글은 GIPS에서 개발한 기술들을 오픈 소스화하면서 업계의 합의를 얻기위한 IETF와 W3C에서 해당 기술과 관련된 표준들의 중심으 뛰어듭니다. 2011년 5월, Ericsson이 첫번째 WebRTC 데모를 개발합니다.

WebRTC는 현재 실시간, 플러그인 필요없는 영상과 음성, data 통신에 대한 공개 표준들을 구현했습니다. 현실의 요구들입니다:

  • 많은 웹서비스들은 이미 실시간 통신을 사용하고 있습니다. 하지만 네이티브 앱이나 플러그인들의 다운로드가 필요합니다. Skype, (Skype를 사용하는)Facebook (Google Talk 플러그인을 사용하는)Google Hangouts등이 그렇습니다.
  • 다운로딩, 설치 그리고 플러그인들을 업데이트 하는 것들은 복잡할 수 있고 애러를 발생시키며 귀찮은 일입니다.
  • 플러그인들을 배포하고, 디버깅하고 문제잡고 테스트하고 유지하는 것은 힘든 일입니다. 그리고 아마도 복잡하고 비싼 기술들에 대한 라이센스나 구현이 필요할 수 도 있습니다. 사용자들에게 플러그인 첫 설치를 독려하는 것은 정말 어려운 일입니다!

WebRTC 프로젝트의 API들은 오픈소스이고, 무료이고, 표준화되어야하고 웹브라우저 안에서 만들어져야고, 이전의 기술들보다 효율적이어야한다는 것이 WebRTC 프로젝트의 안내 지침 입니다.

현재 어디까지?

WebRTC에는 세가지 API가 구현되어 있습니다:

getUserMedia는 Chrome, Opera 그리고 Firefox에서 가능합니다. 크로스 브라우저간의 데모 - simpl.info/gum 와 Web Audio의 입력으로 getUserMedia 사용한 Chris Wilson의 환상적인 예제를 둘러보시기 바랍니다.

RTCPeerConnection은 Chrome(데스크탑과 Android), Opera(데스크탑과 최신 안드로이드 beta)와 Firefox에서 구현되어있습니다. 이름으로 쓰이는 임시 단어가 여러번 반복된 이후에 현재 크롬에서는 RTCPeerConnectionwebkitRTCPeerConnection으로 구현되었고, Firefox에서는 mozRTCPeerConnection로 구현되었습니다. 다른 이름들과 구현들은 더 이상 사용되지 않게 되었습니다. 표준화 과정이 안정화될 때, 머릿말들은 모두 삭제될 것입니다. 여기 아주 간단한 크롬의 RTCPeerConnection 데모 simpl.info/pc와 아주 훌륭한 비디오챗 프로그램 apprtc.appspot.com이 있습니다. 브라우저간의 차이와 명세 변경에 대한 것을 추상화하는 adapter.js - 구글에서 관리하는 Javascript 코드를 이 프로그램에서 사용하고 있습니다.

RTCDataChannel 은 Chrome 25버전, Opera 18버전, Firefox 22버전 이상에서 지원하고 있습니다.

인터넷 익스플로러에서는 Chrome Frame을 통해 WebRTC기능이 사용 가능하고, (2011년에 Microsoft에 흡수된) Skype에서는 WebRTC를 사용려고 계획 중입니다. WebRTC는 또한 WebKitGTK+Qt 네이티브 앱에 통합되었습니다.

주의할 것

'WebRTC를 지원하는' 플랫폼의 이야기들은 의심을 해봐야합니다. 대부분의 플랫폼들은 RTC 컴포넌트들은 전혀 지원하지 않고 단지 getUserMedia만 지원한다는 뜻 입니다.

My first WebRTC

WebRTC 어플리케이션에는 반드시 해야하는 몇 가지 것들이 있습니다:

  • 스트리밍 오디오, 비디오 또는 데이터를 가져와야 합니다.
  • IP주소, 포트등의 네트워크 정보를 가져와야하고, (peers로 알려진) 다른 WebRTC 클라이언트들과 연결을 위해 이 정보들을 교환해야 합니다. 심지어 NATs와 방화벽을 통해서도 교환해야 합니다.
  • 애러들의 보고, 세션들의 초기화와 종료를 위한 신호 통신을 관리해야 합니다.
  • 해상도와 코덱들 같은 미디어와 클라이언트의 capabilty에 대한 정보를 교환해야 합니다.
  • 스트리밍 오디오, 비디오 또는 데이터를 주고 받아야합니다.

스트리밍 데이터를 얻고 통신하기위애 WebRTC에서는 다음과 같은 API들을 제공합니다:

  • MediaStream: 사용자의 카메라와 마이크 같은 곳의 데이터 스트림에 접근합니다.
  • RTCPeerConnection: 암호화 및 대역폭 관리를 하는 기능을 가지고 있고, 오디오 또는 비디오 연결을 합니다.
  • RTCDataChannel: 일반적인 데이터 P2P통신

(뒤에서 WebRTC의 네트워크와 신호에 관해 자세하게 다룹니다.)

MediaStream (aka getUserMedia)

MediaStream API는 미디어의 동기화된 스트림들을 말합니다. 예를 들어, 카메라와 마이크의 입력에서 받아온 스트림은 오디오와 비디오 트랙들로 동기화 됩니다. (MediaStream의 트랙을 완전히 다른의미의 <track> 요소와 헷갈리면 안됩니다.)

아마도 MediaStream을 이해하는 가장 쉬운 방법은 실제로 눈으로 보는 것일 겁니다:

  1. Chrome 또는 Opera로 데모페이지 simpl.info/gum를 엽니다.
  2. 콘솔을 엽니다.
  3. 글로벌 스코프에 위치한 스트림 변수를 검사합니다.

각 MediaStream은 navigator.getUserMedia()에서 생성된 입력과, <video> 태그 또는 RTCPeerConnection로 넘겨주는 출력을 가지고 있습니다.

getUserMedia() 함수는 3개의 매개 변수를 받습니다:

  • 제약 오브젝트.
  • 성공시, MediaStream을 넘겨 받는 콜백.
  • 실패시, 애러 오브젝트를 넘겨 받는 콜백.

각 MediaStream은 'Xk7EuLhsuHKbnjLWkW4yYGNJJ8ONsgwHBvLQ'과 같은 라벨을 가집니다. getAudioTracks()getVideoTracks()함수는 MediaStreamTracks의 배열을 반환합니다.

simpl.info/gum 예제에서, (오디오가 없기때문에) stream.getAudioTracks()은 빈 배열을 반한다. 그리고 웹캠이 연결되어있다고 가정하면, stream.getVideoTracks()은 웹캠 스트림을 나타내는 MediaStreamTrack 하나의 배열을 반환합니다. 각 MediaStreamTrack은 (비디오 또는 오디오) 종류와 ('FaceTime HD Camera (Built-in)'과 같은) 라벨을 가지고, 오디오 또는 비디오의 하나 이상의 채널을 나타냅니다. 이런 경우는, 비디오 트랙 하나가 있고 오디오가 없는 경우이지만, 여러 사용처를 쉽게 상상할 수 있습니다: 예를 들어, 전면/후면 카메라, 마이크와 화면 공유 프로그램으로 부터 스트림들을 얻는 채팅 프로그램.

Chrome 또는 Opera에서는 URL.createObjectURL() 함수가 MediaStream을 video 요소의 src에 설정 가능한 Blob URL로 변경합니다. (Firefox와 Opera에서는, video의 src에 스트림 자체를 설정할 수 있습니다. ) 25버전 이후부터, Chrome은 getUserMedia에서 얻은 오디오 데이터를 audio 또는 video 태그에 넘겨줄 수 있습니다.(그러나 이경우에는 기본적으로 미디어 요소는 묵음 상태임을 알아야 합니다.)

getUserMedia 또한 Web Audio API의 입력노드로 사용될 수 있습니다:

function gotStream(stream) {
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
    var audioContext = new AudioContext();

    // Create an AudioNode from the stream
    var mediaStreamSource = audioContext.createMediaStreamSource(stream);

    // Connect it to destination to hear yourself
    // or any other node for processing!
    mediaStreamSource.connect(audioContext.destination);
}

navigator.getUserMedia({audio:true}, gotStream);

크로미엄 기반의 앱과 확장 프로그램에도 getUserMedia가 포함되어있습니다. audioCapturevideoCapture 권한들을 manifest에 추가하면 설치시 단한번의 요청을 수락하는 것으로 권한을 활성화 시킵니다. 그 이후에는 카메라와 마이크의 접속 권한에 대하여 묻지않습니다.

비슷하게, 페이지에서 HTTPS를 사용하는 경우: (적어도 Chrome에서는) getUserMedia()에 대한 권한을 한번만 혀용하면 됩니다. 처음에는 브라우저의 상태바에 '항상 허용' 버튼이 표시됩니다.

결국 의도는 카메라와 마이크 뿐만 아니라 모든 스트리밍 데이터 소스에 대한 MediaStream을 활성화하는 것입니다. 이것은 디스크로부터 스트리밍을 활성화하거나, 센서들 또는 다름 입력기기와 같은 임의의 데이터 소스들로부터 스트리밍을 활성화하는 것입니다.

getUserMedia()는 반드시 로컬 파일 시스템이 아닌 서버에서 사용되어야하며, 이외의 경우에는 PERMISSION_DENIED: 1 에러가 발생한다는 점을 숙지해야합니다.

getUserMedia()은 다른 Javascript API, 라이브러리들과 함께 우리앞에 바짝 다가와 있습니다:

  • Webcam Toy 은 WebGL을 사용한 포토부스 앱으로, 특이하고 아름다운 효과들이 적용된 사진을 공유하거나 로컬에 저장할 수 있습니다.
  • FaceKatheadtrackr.js를 사용하여 '얼굴 추적'게임을 만들었습니다.
  • ASCII Camera 는 Canvas API 를 사용하여 ASCII 이미지를 생성합니다.
ASCII image generated by idevelop.ro/ascii-camera
gUM ASCII art!

Constraints

Constraints Chrome 24버전, Opera 18버전 이후 부터 구현이 되어있습니다. 이것은 getUserMedia() 와 RTCPeerConnection addStream() 호출의 video 해상도를 위한 값들을 설정하기 위해 사용할 수 있습니다. 목표는 applyConstraints() 함수와 함께 화면비율, 화면모드(앞 또는 뒤 카메라), 프레임 속도, 높이와 너비와 같은 다른 constraints를 지원하도록 구현하는 것입니다.

simpl.info/getusermedia/constraints에 예제가 있습니다.

One gotcha: getUserMedia 의 constraints를 한 탭에서 설정을 하면 이후 열리는 탭들에도 적용이됩니다. 값을 '거부'로 설정하게되면 애러 메세지를 얻게됩니다:

navigator.getUserMedia error:
NavigatorUserMediaError {code: 1, PERMISSION_DENIED: 1}

Screen and tab capture

화면 캡쳐 역시 MediaStream처럼 사용할 수 있습니다. 이 기능은 현재 이 데모 처럼 experimental chromeMediaSource constraint가 설정된 크롬에서만 동작하고 있습니다. 이 기능은 Opera에서는 아직 지원하지 않습니다. 그리고 화면 캡쳐기능은 HTTPS상에서만 동작합니다.

크롬앱에서 또한 experimental chrome.tabCapture API를 통해서 단일 탭의 화면을 라이브 '비디오'로 공유가 가능합니다. 화면 공유에 관한 코드와 더 많은 정보를 확인하려면 HTML5Rocks의 Screensharing with WebRTC을 참고하시기 바랍니다. 이기능은 Opera에서는 아직 지원되지 않습니다.

Signaling: session control, network and media information

WebRTC (피어들로 불리우는) 브라우저들 사이에 스트리밍 데이터를 주고 받는 RTCPeerConnection를 사용합니다, 그리고 또한 통신을 조율하고 조장할 메세지를 주고 받기 위해 Signaling으로 알려진 일련의 과정이 필요합니다. Signaling을 위한 방법들과 프로토콜들은 WebRTC에는 명세되어있지 않습니다 : Signaling은 RTCPeerConnection API에 포함되지 않습니다.

대신, WebRTC 개발자들은 SIP,XMPP 도는 적절한 쌍방통신 채널 등 자신들에게 편한 방식을 선택할 수 있습니다. apprtc.appspot.com 예제는 Signaling 방식으로 XHR과 Channel API를 사용하였습니다. codelabNode server위에 Socket.io를 이용해서 만들여졌습니다.

Signaling은 3가지 종류의 정보를 교환합니다.

  • Session control messages: 통신의 초기화, 종료 그리고 애러 리포트를 위해.
  • Network configuration: 외부 세상으로 내 컴퓨터의 IP 주소와 Port는 어떻게되지 ?
  • Media capabilities: 내 브라우저와 상대브라우저가 사용 가능한 코덱들과 해상도들은 어떻게 되지?

Signaling을 통한 정보 교환은 p2p streaming이 시작되기 전에 반드시 성공적으로 완료되어야 합니다.

예를들어 Alice가 Bob과 통신하기를 원한다고 상상해봅시다. Signaling 과정을 보여주는 WebRTC W3C Working Draft에 따라 예제 코드는 아래와 같습니다. 이 코드는 이미 특정 Signaling 방식이 존재한다고 가정하에 createSignalingChannel() 함수를 만들었습니다. 또한 Chrome과 Opera에서는 RTCPeerConnection가 prefix를 가지고 있다는 점을 유의해야합니다.

var signalingChannel = createSignalingChannel();
var pc;
var configuration = ...;

// run start(true) to initiate a call
function start(isCaller) {
    pc = new RTCPeerConnection(configuration);

    // send any ice candidates to the other peer
    pc.onicecandidate = function (evt) {
        signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
    };

    // once remote stream arrives, show it in the remote video element
    pc.onaddstream = function (evt) {
        remoteView.src = URL.createObjectURL(evt.stream);
    };

    // get the local stream, show it in the local video element and send it
    navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
        selfView.src = URL.createObjectURL(stream);
        pc.addStream(stream);

        if (isCaller)
            pc.createOffer(gotDescription);
        else
            pc.createAnswer(pc.remoteDescription, gotDescription);

        function gotDescription(desc) {
            pc.setLocalDescription(desc);
            signalingChannel.send(JSON.stringify({ "sdp": desc }));
        }
    });
}

signalingChannel.onmessage = function (evt) {
    if (!pc)
        start(false);

    var signal = JSON.parse(evt.data);
    if (signal.sdp)
        pc.setRemoteDescription(new RTCSessionDescription(signal.sdp));
    else
        pc.addIceCandidate(new RTCIceCandidate(signal.candidate));
};

우선 Alice와 Bob은 네트워크 정보를 교환합니다. ('finding candidates'란 표현은 ICE framework을 사용해 네트워크 인터페이스와 포트를 찾는 과정을 뜻합니다.)

  1. Alice가 onicecandidate 핸들러를 가진 RTCPeerConnection object 를 생성합니다.
  2. 핸들러는 네트워크 candidates가 가능해지면 실행됩니다.
  3. Alice는 serialized된 candidate data를 WebSocket 또는 어떤 특정 방식등 그 들만의 Signaling 채널을 통해 Bob에게 전송합니다.
  4. Bob이 Alice로부터 candidate 메세지를 받으면 Bob은 상대방 peer description을 위한 candidate를 추가하기위해 addIceCandidate를 호출합니다.

(Alice와 Bob 같은 피어들로 불리우는) WebRTC 클라이언트들은 해상도와 코덱 기능 같은 미디어 정보들을 서로 교환하고 확인해야합니다. 미디어 정보의 교환을 위한 Signaling은 Session Description Protocol (SDP)를 사용하는 offeranswer를 통해서 진행됩니다:

  1. Alice가 RTCPeerConnection의 createOffer() 함수를 호출합니다. 이 함수의 argument로 입력된 callback을 통해 Alice의 Session description 정보를 나타내는 RTCSessionDescription을 얻습니다.
  2. callback 안에서 Alice는 자신의 description을 setLocalDescription() 함수로 설정하고 이 session description을 signaling 채널을 통해 Bob에게 전달합니다. setLocalDescription()가 호출되기 전까지는 candidates 수집이 시작되지 않는 다는 점을 주의해야 합니다: 이것은 JSEP IETF draft에 문서화 되어있습니다.
  3. Bob은 Alice가 보낸 session description 정보를 setRemoteDescription() 함수를 통해 설정합니다.
  4. Bob은 RTCPeerConnection의 createAnswer() 함수에 Alice로 부터 받은 remote description을 전달하고 실행합니다. 그러면 그녀의 정보를 기반으로 Local Session이 생성됩니다. createAnswer()의 callback은 RTCSessionDescription을 전달해줍니다: Bob은 이것을 local description으로 설정하고 Alice에게 전달합니다.
  5. Alice가 Bob의 Session description을 받으면 setRemoteDescription을 이용해 remote descirption으로 설정합니다.
  6. Ping!

RTCSessionDescription 객채들은 Session Description Protocol, SDP를 따르는 blob입니다. 직렬화된 SDP 객체는 다음과 같이 보입니다:

v=0
o=- 3883943731 1 IN IP4 127.0.0.1
s=
t=0 0
a=group:BUNDLE audio video
m=audio 1 RTP/SAVPF 103 104 0 8 106 105 13 126

// ...

a=ssrc:2223794119 label:H4fjnMzxy3dPIgQ7HxuCTLb4wLLLeRHnFxh810

네트워크와 미디어 정보의 교환과 획득은 동시에 완료됩니다, 그러나 이 두가지 과정들은 피어간의 오디오/비디오 스트리밍 시작전에 반드시 완료 되어야합니다.

위에서 설명된 offer/answer 아키텍쳐는 JSEP, JavaScript Session Establishment Protocol 이라고 불리웁니다. (Ericsson's demo video은 Ericsson에서 첫번째 WebRTC 구현할 당시 signaling과 streaming 과정을 아주 훌륭하게 설명한 애니메이션 입니다.)

JSEP architecture diagram
JSEP architecture

일단 signaling 과정이 성공적으로 마치면 data는 peer와 peer 끼리 또는 송신자와 수신자가 직접 주고 받게됩니다.—만약 이것이 실패하게되면, 중계서버를 통하게 됩니다.(뒤에 자세하게 다루겠습니다.) 스트리밍이 RTCPeerConnection의 역할입니다.

RTCPeerConnection

RTCPeerConnection은 Peer들 간의 데이터를 안정적이고 효율적으로 통신하게 처리하는 WebRTC 컴포넌트입니다.

아래는 RTCPeerConnection의 역할을 보여주는 WebRTC 아키텍쳐 다이어그램입니다. 보시는 것처럼 녹색 부분들은 복잡합니다!

WebRTC architecture diagram
WebRTC architecture (from webrtc.org)

JavaScript 측면에서 보면, 이 다이어그램을 통해 알 수 있는 중요한 점은 RTCPeerConnection이 뒤에 숨겨진 매우 복잡한 것들을 웹개발자로부터 지켜주고 있다는 것 입니다. WebRTC에서 사용되는 코덱들과 프로토콜들은 불안정한 네트워크 위에서도 실시간 통신이 가능하게 하기위해 엄청난 양의 일들을 하고 있습니다:

  • packet loss concealment
  • echo cancellation
  • bandwidth adaptivity
  • dynamic jitter buffering
  • automatic gain control
  • noise reduction and suppression
  • image 'cleaning'.

위에 나온 W3C 코드는 Signaling 측면에서 WebRTC의 간소화된 예를 보여줍니다. 다음은 현재 동작하고 있는 두가지 WebRTC 어플리케이션 입니다: 첫 번째는 단순한 RTCPeerConnection의 데모 예제이고, 두번째는 온전히 동작하는 화상채팅 클라이언트 입니다.

RTCPeerConnection without servers

다음의 코드는 '단일페이지' WebRTC 데모 - webrtc-demos.appspot.com에서 발췌한 것입니다, 이것은 로컬 원격지의 RTCPeerConnection (그리고 로컬과 원격지의 비디오)가 같은 웹페이지 위에 있습니다. 이 예제안에는 유용한 것들은 없습니다, 하지만 RTCPeerConnection 간의 중간 signaling 과정을 사용하지 않고 직접 페이지에서 데이터와 메세지를 주고 받기 때문에 RTCPeerConnection API의 동작을 좀 더 단순하게 보여줍니다.

One gotcha: RTCPeerConnection()의 생성자에서 두번째 파라메터로 사용되는 constraints는 getUserMedia()에서 사용되는 것과는 차이가 있습니다: w3.org/TR/webrtc/#constraints에서 더 많은 정보를 확인바랍니다.

이 예제에서, pc1은 local 피어(Caller)를 나타내고, pc2는 원격지 peer(Callee)를 나타냅니다.

Caller

  1. RTCPeerConnection을 새로 생성하고 getUserMedia()로 부터 스트림을 얻습니다:

    // servers is an optional config file (see TURN and STUN discussion below)
    pc1 = new webkitRTCPeerConnection(servers);
    // ...
    pc1.addStream(localstream); 
  2. offer를 생성하고 pc1의 description을 설정합니다. 그리고 pc2의 것을 remote description을 설정합니다. 여기서는 Caller와 Callee가 같은 페이지에 있기 때문에 signaling없이 직접 코드로 연결할 수 있습니다:

    pc1.createOffer(gotDescription1);
    //...
    function gotDescription1(desc){
      pc1.setLocalDescription(desc);
      trace("Offer from pc1 \n" + desc.sdp);
      pc2.setRemoteDescription(desc);
      pc2.createAnswer(gotDescription2);
    }
    

Callee

  1. pc2를 생성하고, pc1에 스트림이 추가되면 그것을 video 요소에 표시합니다:

    pc2 = new webkitRTCPeerConnection(servers);
    pc2.onaddstream = gotRemoteStream;
    //...
    function gotRemoteStream(e){
      vid2.src = URL.createObjectURL(e.stream);
    }
    

RTCPeerConnection plus servers

현실 세계에서는 WebRTC는 서버가 필요하지만 단순합니다. 그래서 다음과 같은 것이 가능합니다:

  • 사용자들은 상대방을 발견하고 이름과 같은 '현실 세계'의 상세를 교환합니다.
  • WebRTC 클라이언트 어플리케이션들(피어들)은 네트워크 정보를 교환합니다.
  • 피어들은 비디오 포맷과 해상도 같은 미디어에 관한 데이터를 교환합니다.
  • WebRTC 클라이언트 어플리케이션들은 NAT gateways와 방화벽을 넘나듭니다.

다른 말로, WebRTC는 4가지 종류의 서버측 기능들이 필요합니다:

  • 사용자 탐색과 통신.
  • Signaling.
  • NAT/firewall 탐색.
  • P2P 실패시의 중계서버들.

NAT 탐색, peer-to-peer 네트워크, 사용자 탐색과 Signaling의 서버앱을 만들기 위한 요구사항들은 본문에서는 다루지 않습니다. STUN 프로토콜과 그 확장인 TURN은 NAT 탐색과 다른 네트워크 문제들을 처리하기위해 RTCPeerConnection을 사용 가능하게 하는 ICE framework를 사용한다는 설명으로 충분합니다.

ICE 비디오 채팅 클라이언트 같은 피어들을 연결하기위한 프레임워크입니다. 우선, ICE는 가장 대기시간이 적은 UDP를 통해 피어들 끼리 directly 연결 가능한지 시도합니다. 여기에서 STUN 서버들은 한가지일을 합니다: NAT 뒤에 있는 피어들이 연결 가능하도록 자신들의 공용(public)주소와 포트를 찾아줍니다. (Google은 몇개의 STUN 서버를 가지고 있고,the apprtc.appspot.com 예제에서 그 중 하나를 사용중입니다.)

Finding connection candidates
Finding connection candidates

만약 UDP가 실패하면, ICE는 HTTP상의 TCP로 시도를 하고 그 다음엔 HTTPS상에서 시도합니다. 만약 직접 연결이 실패하면 —간혹 NAT와 방화벽으로 인해—ICE는 중계를 위해 TURN 서버를 사용합니다. 다시말해, ICE는 ICE는 처음에는 피어들간에 직접연결을 위해 UDP를 이용한 STUN서버를 사용하다가 실패하면 TURN 중계 서버를 통합니다. 'finding candidates'란 표현은 네트워크 장치와 포트들을 찾는 과정을 의미합니다.

WebRTC data pathways
WebRTC data pathways

WebRTC 개발자 Justin Uberti는 2013 Google I/O WebRTC presentation에서 ICE, STUN 그리고 TURN에 관한 더 많은 정보를 알려주었습니다. (발표 슬라이드 TURN과 STUN 서버 구현 예제들을 보여줍니다.)

A simple video chat client

뒤에 나오는 signaling 매커니즘은 apprtc.appspot.com에서 사용된 것 입니다.

만약 여기서 이해할 수 없는 부분이 있다면, WebRTC codelab을 참고하는 것이 더 좋을 것입니다. 이 단계별 가이드는 어떻게 화상 채팅 어플리케이션을 만드는지 설명합니다, 그리고 Socket.io를 사용해서 Node server에서 구현한 Signaling 서버도 포함되어 있습니다.

WebRTC를 체험해보기 좋은 곳은 signaling도 구현되었고 STUN을 사용해 NAT/방화멱 탐색까지 구현된 apprtc.appspot.com입니다. 이 어플리케이션에서는 RTCPeerConnection과 getUserMedia()의 브라우저별 구현 차이를 처리하기 위해 adapter.js를 사용 했습니다.

코드에서는 의도적으로 장황하게 로그를 남겼습니다: 이벤트들의 순서를 이해하기위해서는 콘솔을 확인하시기 바랍니다. 뒤에 자세한 코드 예제들을 보여드리겠습니다.

What's going on?

데모는 initalize() 함수를 시작으로 실행됩니다:

function initialize() {
    console.log("Initializing; room=99688636.");
    card = document.getElementById("card");
    localVideo = document.getElementById("localVideo");
    miniVideo = document.getElementById("miniVideo");
    remoteVideo = document.getElementById("remoteVideo");
    resetStatus();
    openChannel('AHRlWrqvgCpvbd9B-Gl5vZ2F1BlpwFv0xBUwRgLF/* ...*/');
    doGetUserMedia();
  }

openChannel()에서 사용되는 room 값과 token은 구글 앱엔진에서 제공되는 것들 입니다: 어떤 값들이 추가되었는지를 확인하기 위해서는 레포지토리 안에 있는 index.html template를 확인하시기 바랍니다.

이 코드는 로컬 카메라의 (localVideo)와 원격지 카메라의 (remoteVideo)가 표시될 HTML video 요소들을 변수들을 초기화합니다. resetStatus()는 단순하게 상태 메세지를 설정합니다.

openChannel() 함수는 WebRTC 클라이언트 간의 메세지 환경을 구축합니다:

function openChannel(channelToken) {
  console.log("Opening channel.");
  var channel = new goog.appengine.Channel(channelToken);
  var handler = {
    'onopen': onChannelOpened,
    'onmessage': onChannelMessage,
    'onerror': onChannelError,
    'onclose': onChannelClosed
  };
  socket = channel.open(handler);
}

이 데모에서는 signaling을 위해 polling 없이 JavaScript 클라이언트 간에 메세징이 가능한 Google App Engine의 Channel API를 사용합니다. (WebRTC Signaling에 대한 자세한 내용은 이미 위에서 설명하였습니다).

Architecture of the apprtc video chat application
Architecture of the apprtc video chat application

Channel API를 통해 채널을 생성하는 방식은 다음과 같습니다:

  1. 클라이언트 A가 하나의 유니크 ID를 생성합니다.
  2. 클라이언트 A는 자신의 ID를 App Engine 앱에 넘겨주면서 채널 토큰을 요청합니다.
  3. App Engine 앱은 Channel API로 부터 채널과 클라이언트 ID에 해당하는 토큰을 요청합니다.
  4. 앱은 토큰을 클라이언트 A에게 전달합니다.
  5. 클라이언트 A는 소켓을 열고 서버에 설정된 채널로부터 메세지를 기다립니다.
The Google Channel API: establishing a channel
The Google Channel API: establishing a channel

메세지 전송하는 방법은 다음과 같습니다:

  1. 클라이언트 B는 변경사항과 함께 App Engine 앱으로 POST 요청을 보냅니다.
  2. App Engine 앱은 요청을 channel로 전달합니다.
  3. 채널은 메세지를 클라이언트 A에게 전달합니다.
  4. 클라이언트 A의 onmessage 콜백함수가 호출됩니다.
The Google Channel API: sending a message
The Google Channel API: sending a message

다시 말하지만: Signaling 메세지는 개발자가 선택한 매커니즘을 통해서 통신하게됩니다. Signaling 메커니즘은 WebRTC 명세에 포함되어 있지 않습니다. Channel API를 데모에서 사용된 방식이고, (웹소켓과 같은) 다른 방식을 사용할 수 도 있습니다.

openChannel() 함수가 호출된 이후에, initialize()에서 호출되는 getUserMedia() 함수는 브라우저에서 getUserMedia API가 지원되는지 확인합니다. (getUserMedia에 대한 자세한 내용은 HTML5 Rocks에서 확인 바랍니다.) 지원이 된다면, onUserMediaSuccess 콜백이 호출됩니다:

function onUserMediaSuccess(stream) {
  console.log("User has granted access to local media.");
  // Call the polyfill wrapper to attach the media stream to this element.
  attachMediaStream(localVideo, stream);
  localVideo.style.opacity = 1;
  localStream = stream;
  // Caller creates PeerConnection.
  if (initiator) maybeStart();
}

이를 통해 로컬 카메라로 부터 받은 비디오를 localVideo에 표시하게 됩니다, 이때 카메라의 데이터 스트림으로 부터 object (Blob) URL가 만들어지고 요소의 src에 URL을 설정합니다. (createObjectURL이 이때 사용되어져 '메모리상'에 있는 바이너리 리소스,즉, 비디오를 위한 LocalDataStream의 URI를 얻게됩니다.) 데이터 스트림은 또한 localStream 변수에 설정되어 이후 원격지 사용자를 위해 사용할 수 있게 됩니다.

이 시점에, initiator의 값에 1이 설정됩니다. (그리고 이 값은 Caller의 세션이 종료될 때까지 유지됩니다.) 그래서 maybeStart()가 호출됩니다:

function maybeStart() {
  if (!started && localStream && channelReady) {
    // ...
    createPeerConnection();
    // ...
    pc.addStream(localStream);
    started = true;
    // Caller initiates offer to peer.
    if (initiator)
      doCall();
  }
}

이 함수는 비동기 다중 콜백들을 처리할때 편리합니다: maybeStart()은 여러 함수들 중 하나로 부터 호출될 것이지만, 코드는 localStream정의되고 channelReady의 값이 true가 되고 통신이 아직 시작되지 않은 경우에만 실행됩니다. 그래서—만약 연결이 이미 되었고, 로컬 스트림이 사용가능하고, Signaling을 위한 채널이 준비가 되었다면, 연결이 생성되고 로컬 비디오 스트림이 전달됩니다. 이것이 한번 진행되면, started는 true가 되어, 연결은 더이상 시작되지 않습니다.

RTCPeerConnection: making a call

maybeStart()에서 호출된createPeerConnection()이 진짜 액션의 시작입니다:

function createPeerConnection() {
  var pc_config = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
  try {
    // Create an RTCPeerConnection via the polyfill (adapter.js).
    pc = new RTCPeerConnection(pc_config);
    pc.onicecandidate = onIceCandidate;
    console.log("Created RTCPeerConnnection with config:\n" + "  \"" +
      JSON.stringify(pc_config) + "\".");
  } catch (e) {
    console.log("Failed to create PeerConnection, exception: " + e.message);
    alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser.");
      return;
  }

  pc.onconnecting = onSessionConnecting;
  pc.onopen = onSessionOpened;
  pc.onaddstream = onRemoteStreamAdded;
  pc.onremovestream = onRemoteStreamRemoved;
}

다음은 onIceCandidate() 콜백을 통해 STUN 서버를 사용한 연결을 설정하기 위한 것입니다.(위에서 설명한 ICE, STUN ,'candidate'을 참고하시기 바랍니다). 핸들러들은 RTCPeerConnection에서 발생하는 이벤트들을 위해 설정됩니다: 세션이 연결되고 열렸을때, 그리고 원격지 스트림이 추가되고 삭제되었을때. 사실 이 예제에서의 핸들러들은 단순히 로그 메세지만 출력하고 있습니다.—remoteVideo의 소스를 설정하는 onRemoteStreamAdded()만 제외하고:

function onRemoteStreamAdded(event) {
  // ...
  miniVideo.src = localVideo.src;
  attachMediaStream(remoteVideo, event.stream);
  remoteStream = event.stream;
  waitForRemoteVideo();
}

maybeStart()에서 createPeerConnection()이 한번 호출이 되면, 연결이 생성되어 callee에게 전달이 됩니다:

function doCall() {
  console.log("Sending offer to peer.");
  pc.createOffer(setLocalAndSendMessage, null, mediaConstraints);
}

offer가 생성되는 과정은 위에서나온 no-signaling 예제와 비슷하지만 추가적으로 offer를 위해 serialized된 SessionDescription을 원격지 피어에게 메세지로 전달 합니다. 이 과정은 setLocalAndSendMessage()를 통해 진행됩니다:

function setLocalAndSendMessage(sessionDescription) {
  // Set Opus as the preferred codec in SDP if Opus is present.
  sessionDescription.sdp = preferOpus(sessionDescription.sdp);
  pc.setLocalDescription(sessionDescription);
  sendMessage(sessionDescription);
}

Signaling with the Channel API

onIceCandidate() createPeerConnection()에서 성공적으로 RTCPeerConnection이 생성되었을 떄 호출되는 onIceCandidate() 콜백은 candidates에 관해 '획득한' 정보를 전달합니다:

function onIceCandidate(event) {
    if (event.candidate) {
      sendMessage({type: 'candidate',
        label: event.candidate.sdpMLineIndex,
        id: event.candidate.sdpMid,
        candidate: event.candidate.candidate});
    } else {
      console.log("End of candidates.");
    }
  }

클라이언트에서 서버로 송출되는 메세지는 XHR을 이용한 sendMessage()가 처리합니다:

function sendMessage(message) {
  var msgString = JSON.stringify(message);
  console.log('C->S: ' + msgString);
  path = '/message?r=99688636' + '&u=92246248';
  var xhr = new XMLHttpRequest();
  xhr.open('POST', path, true);
  xhr.send(msgString);
}

XHR은 클라이언트에서 서버로 signaling 메세지들을 잘 전달합니다, 하지만 일부 메커니즘은 서버에서 클라이언트로의 메세지처리가 필요합니다: 이 어플리케이션에서는 Google App Engine Channel API를 사용합니다. API(즉, App Engine Server)로 부터 받은 메세지는 processSignalingMessage()에서 처리됩니다:

function processSignalingMessage(message) {
  var msg = JSON.parse(message);

  if (msg.type === 'offer') {
    // Callee creates PeerConnection
    if (!initiator && !started)
      maybeStart();

    pc.setRemoteDescription(new RTCSessionDescription(msg));
    doAnswer();
  } else if (msg.type === 'answer' && started) {
    pc.setRemoteDescription(new RTCSessionDescription(msg));
  } else if (msg.type === 'candidate' && started) {
    var candidate = new RTCIceCandidate({sdpMLineIndex:msg.label,
                                         candidate:msg.candidate});
    pc.addIceCandidate(candidate);
  } else if (msg.type === 'bye' && started) {
    onRemoteHangup();
  }
}

만약 피어로부터 (offer에 대한 응답으로) 온 메세지가 answer 라면, RTCPeerConnection은 원격지 SessionDescription을 설정하고 통신을 시작합니다. 만약 메세지가 (즉 callee로 부터온) offer라면, RTCPeerConnection는 원격지 SessionDescription을 설정하고 the callee에게 answer를 보냅니다. 그리고 RTCPeerConnection startIce()함수를 호출하여 연결을 시작합니다:

function doAnswer() {
  console.log("Sending answer to peer.");
  pc.createAnswer(setLocalAndSendMessage, null, mediaConstraints);
}

이게 끝입니다! caller와 callee는 서로를 발견하고 자신들의 기능에 대한 정보를 교환하고, 통화 세션을 초기화합니다. 그러면 실시간 데이터 통신이 시작될 수 있습니다.

Network topologies

WebRTC는 재 일대일 통신에 대한 구현만 되어있습니다, 하지만 좀 더 복잡한 네트워크 시나리오 상에서 사용될 수 있습니다: 예를 들어 여러개의 피어들이 각각 P2P로 직접 통신할 수 있고 , 또는 대량의 참가자들을 처리할 수 있는 Multipoint Control Unit (MCU) 서버를 사용하여 선택적인 스트림을 전달하고, 오디오와 비디오를 믹싱 또는 녹화할 수도 있습니다:

Multipoint Control Unit topology diagram
Multipoint Control Unit topology example

존재하는 많은 WebRTC앱들은 단지 웹 브라우저간의 통신만 보여주고 있습니다, 하지만 게이트웨이 서버들도 브라우저 상에 WebRTC 앱을 실행시켜 전화기 (PSTN으로 불리우는) 장비들 또는 VOIP 시스템들과 동작할 수 있습니다. 2012년 5월에는, sipml5 SIP client를 오픈 소스화한 Doubango Telecom에서 WebRTC와 (다른 용도로도 사용가능한) WebSocket을 이용해 iOS와 Android에서 실행되는 앱과 브라우저들간에 화상통화가 가능하게 했습니다. Google I/O에서, Tethr와 Tropo는 WebRTC를 통해 피쳐폰과 컴퓨터가 통신할 수 있게 해주는 OpenBTS cell를 사용해 '간단하게' a framework for disaster communications를 전시하였습니다. 캐리어 없는 전화 통신!

Tethr/Tropo demo at Google I/O 2012
Tethr/Tropo: disaster communications in a briefcase

RTCDataChannel

오디오와 비디오처럼, WebRTC는 실시간으로 다른 형태의 데이터 통신도 지원합니다.

RTCDataChannel API는 피어와 피어간 임의의 데이터 교환을 빠른 반응속도와 높은 처리량으로 가능하게 합니다. 여기 단순한 '단일 페이지' 데모 simpl.info/dc가 있습니다.

이 API에 대한 많은 잠재적 사용 예가 있습니다:

  • 게임
  • 원격 데스크탑 어플리케이션
  • 실시간 문자 채팅
  • 파일 전송
  • 분산 네트워크

이 API는 RTCPeerConnection의 대부분의 기능들을 활용하여 강력하고 유연한 P2P통신을 가능하게 하는 몇가지 기능을 가지고 있습니다:

  • RTCPeerConnection 세션 설정의 레버리징.
  • 우선순위가 있는 여러개의 동시 채널
  • 신뢰/비신뢰 전달 시멘틱.
  • 빌트인 보안(DTLS) 과 혼잡 제어.
  • 오디오 또는 비디오 유/무 사용.

문법은 send() 함수와 message 이벤트를 사용하는 것이 WebSocket과 거의 유사합니다:

var pc = new webkitRTCPeerConnection(servers,
  {optional: [{RtpDataChannels: true}]});

pc.ondatachannel = function(event) {
  receiveChannel = event.channel;
  receiveChannel.onmessage = function(event){
    document.querySelector("div#receive").innerHTML = event.data;
  };
};

sendChannel = pc.createDataChannel("sendDataChannel", {reliable: false});

document.querySelector("button#send").onclick = function (){
  var data = document.querySelector("textarea#send").value;
  sendChannel.send(data);
};

통신은 브라우저간 직접 연결됩니다 , 그래서 RTCDataChannel은 WebSocket보다 매우 빠릅니다. 심지어 방화벽과 NAT의 방해로 '구멍내기'가 실패하여 중계(TURN) 서버와 연결이 되더라도 빠릅니다.

RTCDataChannel은 Chrome과 Opera, Firefox에서 가능합니다. 멋진 Cube Slam 게임에서는 게임 상태 통신을 위해 이 API를 사용하였습니다: 친구 또는 곰과 플레이 해봅시다! Sharefest enables file sharing via RTCDataChannel, and peerCDN offers a glimpse of how WebRTC could enable peer-to-peer content distribution.

RTCDataChannel에 관한 더 자세한 내용은 IETF의 draft protocol spec문서를 참고하기 바랍니다.

Security

실시간 통신 어플리케이션이나 플러그인에서 보안 문제가 발생하는 경우가 몇가지가 있습니다. 예를 들면:

  • 암호화 되지 않은 미디어나 데이터는 브라우저간 사이에서 또는 브라우저와 서버 사이에서 도둑질 당할수 있습니다.
  • 어떤 어플리케이션은 상대방이 모르는 사이에 비디오 또는 오디오가 녹화되거나 공유될 수 있습니다.
  • 말웨어나 바이러스가 플러그인 또는 어플리케이션안에 숨어 설치될 수 있습니다.

WebRTC는 이 문제들을 피할 수 있는 몇가지 기능들을 가지고 있습니다:

  • WebRTC는 DTLSSRTP등의 보안 프로토콜을 사용하여 구현되었습니다.
  • 암호화는 Signlaing 메커니즘을 포함한 모든 WebRTC Components의 필수 조건입니다.
  • WebRTC는 플러그인이 아닙니다: 컴포넌트들은 브라우저의 샌드박스위에서 실행되고 별도의 프로세스로 나눠지지 않습니다. 컴포넌트들은 별도의 설치가 필요하지 않고 브라우저 없데이트시에 업데이트 됩니다.
  • 카메라와 마이크에 접근은 반드시 허가를 통합니다. 그리고 카메라와 마이크가 사용 중인 경우에는, UI를 통해 명확하게 알려줍니다.

보안에 관한 전체적인 논의는 본문의 범위에 포함되지 않습니다. 더 자세한 정보를 위해서는 IETF에서 제공한 WebRTC Security Architecture를 참고 하시기 바랍니다.

결론

WebRTC의 API들과 표준들은 대중화될 것이고 컨텐츠 생산과 통신을위한 도구를 분산시킬 수 있습니다.—전화, 게임, 영상 제작, 음악 작곡, 뉴스 수집 과 많은 다른 어플리케이션들을 위해서.

기술은 이 이상 분열되지 않을 것 입니다.

우리는 JavaScript 개발자들이 WebRTC를 사용하여 많은 것들을 구현할 것으로 바라보고 있습니다. 블로거 Phil Edholm가 쓴 것 처럼, '잠재적으로, WebRTC와 HTML5은 브라우저가 정보 분야에 줬던 변화만큼 실시간 통신 분야에 변화를 가능하게 할 것이다.'

개발자 도구들

  • 크롬에서 chrome://webrtc-internals 페이지 (또는 오페라에서 opera://webrtc-internals 페이지)는 WebRTC session이 진행중일때 자세한 상태와 차트를 제공합니다:
    chrome://webrtc-internals page
    chrome://webrtc-internals screenshot
  • 크로스 브라우저간의 상호동작에 대한 내용
  • adapter.js는 Google에서 관리하는 WebRTC를 위한 JavaScript 코드 입니다. 이것은 벤더들의 prefix와 브라우저간의 차이들, 명세 구현의 차이들을 추상화합니다.
  • WebRTC Signlaing 과정을 자세하게 확인하려면, apprtc.appspot.com의 콘솔로그를 확인하시기 바랍니다.
  • 만약 이 모든게 너무 많다고 느껴진다면, WebRTC framework 또는 완성된 WebRTC service를 사용하는 것이 좋습니다.
  • 버그 리포트와 기능 요청은 언제나 환영합니다: Chrome 버그들은 crbug.com/new 로, Opera 버그들은 bugs.opera.com/wizard/로, Firefox 버그들은 bugzilla.mozilla.org

더 볼 것들

Standards and protocols

WebRTC 지원 상황 요약

MediaStream and getUserMedia

  • Chrome desktop 18.0.1008+; Chrome for Android 29+
  • Opera 18+; Opera for Android 20+
  • Opera 12, Opera Mobile 12 (based on the Presto engine)
  • Firefox 17+

RTCPeerConnection

  • Chrome desktop 20+ (현재 'flagless', 다시말해 about:flags에 들어갈 필요없음); Chrome for Android 29+ (flagless)
  • Opera 18+ (기본으로 포함됨); Opera for Android 20+ (기본으로 포함됨)
  • Firefox 22+ (기본으로 포함됨)

RTCDataChannel

  • Chrome 25에서 실험버전, Chrome 26+에서 더 안정적이며 (Firefox와 연동됨); Chrome for Android 29+
  • 안정화 버전 (and with Firefox interoperability) in Opera 18+; Opera for Android 20+
  • Firefox 22+ (기본으로 포함됨)

WebRTC는 Internet Explorer에서 Chrome Frame을 통해 사용가능 합니다: demo screencast and links to documentation.

RTCPeerConnection를 위한 Native API들도 있습니다: documentation on webrtc.org.

getUserMedia같은 API들의 크로스 플랫폼 지원 관련 정보는, caniuse.com에서 참고 바랍니다.

Comments

0