Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

HTML5의 WebRTC : PeerConnection

WebRTC의 getUserMedia를 앞의 글에서 살펴보았지만 getUserMedia를 사용하는 것은 뭐 어려울 것도 없다. 정작 헤매기 시작한 것은 PeerConnection을 살펴보기 위한 것인데 제대로 확인해 볼 수 있는 예제가 없어서(여기엔 몇가지 이유가 있지만...) 어떻게 동작하는 것인지 이해하는데 정말 오래걸리고 삽질도 참 많이 했다. P2P 통신에 대해 구현을 해보거나 관련 지식이 어느정도 있다면 좀 쉬울수도 있겠는데 그렇지 않다보면 여러가지 용어부터 익히는데 너무 오래거렸다. Google I/O의 WebRTC의 영상에서 PeerConnection을 시연하는 예제가 나오기는 하지만 PeerConnection을 사용하려면 당연히 서버사이드 구현이 필요한데 여기서는 그냥 Google AppEngine를 사용했기 때문에 정확히 어떻게 구현되는지 감을 잡기가 어려웠다.


JSEP
일단 5월에 PeerConnection의 스펙이 변경되었다. 그래서 그 이전의 PeerConnection과 현재의 PeerConnection은 사용방법이 달라서 과거 스펙으로 구현된 예제들은 사용할 수도 없고 참고도 되지 않는다. 현재의 PeerConnection 스펙은 JSEP(Javascript Session Establishment Protocol)이고 현재 크롬에서 객체명은 PeerConnection00이라고 부른다.(구현이 올라가면서 뒤에 번호를 올릴려는 생각인것 같다.)

사용자 삽입 이미지

PeerConnection을 기본으로 지원하지는 않기 때문에 크롬에서 PeerConnection을 사용하려면(구현체의 적용이 크롬이 가장 빠르므로 크롬에서 테스트하는걸 권장한다.) chrome://flags/에서 위와 같이 PeerConnection 설정을 켜주어야 한다.

PeerConnection을 사용하는 방법을 설명하기 전에 좀 더 얘기하자면 이 글을 쓰는 시점에 크롬의 최신버전은 22인데 23베타부터는 PeerConnection이 기본으로 포함되어 플래그를 변경하지 않고도 바로 사용할 수 있게 되었다. 여기서 더 충격적인 사실은 이렇게 PeerConnection이 추가된뒤에 최신 크롬 카나리에서는 다시 제거되었다. 그래서 현재 최신 크롬 카나리에서는 플래그도 존재하지 않고 PeerConnection00이 완전히 사라졌다. WebRTC의 공지에 따르면 안정버전을 내놓기 전체 큰 변경을 시도한다고 한다. 그래서 크롬 카나리에서 변경된 내용이 나오기 전에 일단 제거된 것이다.

얼마나 바뀔지를 전혀 모르고 있고 일단 현재는 사용할 수 있기 때문에 삽질한 내용을 정리하기는 하지만 PeerConnection의 이러한 진행사항에 대해서는 염두에 두고 있어야 한다. HTML5의 대다수 스펙이 진행중이긴 하지만 이렇게 스펙이 자주 변경된다는 것은 아직 실제로 사용한다는 것은 무리고 테스트삼아 해보는 정도이지만 변경이 공지된 상황에서 그 마저도 언제까지 유효할지 장단하기 어렵다.


Signalling
과정을 설명하기 전에 Signalling을 설명해야 할 것 같다. PeerConnection은 두 명의 유저가 스트림을 주고 받는 것이므로 연결을 요청한 콜러(caller)와 연결을 받는 콜리(callee)가 존재한다. 콜러와 콜리가 통신을 하기 위해서는 중간 역할을 하는 서버가 필요하고 서버를 통해서 SessionDescription을 서로 주고 받아야 한다. P2P 통신과정에 대해서 자세한 내용은 다 이해하지 못하지만 이 SessionDescription에 자신의 네트워크 정보가 들어있는 것으로 보인다.

JSEP 아키텍처

HTML5Rocks의 JSEP 아키텍처(출처: http://www.html5rocks.com/en/tutorials/webrtc/basics/)


그림으로 그리자면 위와 같은데 이게 앞에서 얘기한 JSEP의 아키텍처이다. 여기서 콜러와 콜리는 SessionDescription을 만들어야 하는데 콜러의 SessionDescription를 offer라고 부르고 offer에 대한 응답으로 콜리가 만드는 SessionDescription을 answer라고 부른다.(현재는 1:1 통신만을 의미한다.) 다시 말하면 콜러가 offer를 만들어서 콜리에게 전달하면 콜리가 다시 answer를 만들어서 콜러에게 전달하면 통신을 위한 준비가 완료된다고 할 수 있다.


PeerConnection의 과정
PeerConnection을 사용하기 전에 PeerConnection의 처리과정을 먼저 살펴보자.  일단 콜러부터 보자.

// Caller

// 1. getUserMedia로 localStream 저장
var localStream;
navigator.webkitGetUserMedia(
    {audio:true, video:true}, 
    function(stream) {
        localStream = stream
    }, function() {
    });

// 2. peerConnection 객체 생성
var pc = new webkitPeerConnection00(null, iceCallback);

// 3. 원격 스트림 이벤트 등록
pc.onaddstream = function(e){
  // use e.stream
}

// 4. 로컬스트림 추가
pc.addStream(localStream);

// 5. offer 생성
var offer = pc.createOffer({audio:true, video:true});

// 6. 로컬디스크립션 설정
pc.setLocalDescription(pc.SDP_OFFER, offer);

// 7. offer의 SDP를 전송
// send offer.toSdp();

차례차례 보자.

  1. 먼저 스트림을 획득해야 하므로 이전에 살펴본 getUserMedia로 스트림을 얻어와서 나중에 사용하기 위해 localStream이라는 변수에 저장한다.
  2. 그 다음 peerConnection의 객체를 생성하는데 크롬이므로 webkitPeerConnection00을 사용한다. 스펙이 완전히 확정되면 아마 그냥 peerConnection이 될 것이고 다른 브라우저에서는 mozPeerConnection00이나 oPeerConnection00 등이 될 수 있다. webkitPeerConnection00은 객체를 생성할 때 2개의 파라미터를 받는데 첫 파라미터에는 STUN 서버를 지정하고(여기서는 null로 지정했는데 STUN 서버는 나중에 다시 설명한다.) 두번째 파라미터는 iceCallback이라고 이름을 지정한 콜백함수이다.(이 예제에는 정의를 아직 넣지 않았다. 나중에 설명한다.)
  3. peerConnection 객체에 onaddstream으로 콜백을 등록하는데 나중에 콜러한테 스트림을 받을 때 여기 등록한 콜백에서 스트림을 받는다. 소스처럼 e.stream을 <video> 태그의 소스등으로 지정하면 된다.
  4. peerConnection 객체의 addStrea() 함수를 이용해서 앞에서 얻은 로컬스트림을 등록한다.(로컬스트림을 원격에 전달해야 하므로)
  5. peerConnection 객체로 SessionDescription인 offer를 생성한다.
  6. 생성한 offer를 peerConnection객체의 로컬 디스크립션으로 설정한다.(콜러입장에서는 offer가 로컬이다.)
  7. 준비가 완료되었으므로 offer를 SDP(Session Description Protocol) 형식으로 콜리한테 전송한다.

이번엔 콜리쪽을 보자.

// Callee

// 1. 오퍼로 원격 디스크립션을 설정
pc.setRemoteDescription(pc.SDP_OFFER, new SessionDescription(offer));

// 2. answer 생성
var offer = pc.remoteDescription;
var answer = pc.createAnswer(offer.toSdp(), {has_audio:true, has_video:true});

// 3. 로컬 디스크립션 설정
pc.setLocalDescription(pc.SDP_ANSWER, answer);

// 4. answer의 SDP를 전송
// send answer.toSdp()

//5. ICE 시작
pc.startIce();

서로 화상통화를 하는 상황이므로 콜리도 콜러처럼 스트림을 얻고 peerConnection을 생성하는 등의 앞에서 콜러에서 설명한 4번까지의 과정은 이미 준비했다고 가정한다.

  1. 이 상황에서 콜러가 전달한 offer를 받아서 원격디스크립션을 설정한다.(콜리입장에서는 offer가 원격이다.)
  2. peerConnection에 설정된 원격디스크립션이 offer이므로 이를 변수에 저장하고 offer를사 용해서 answer를 생성한다.
  3. 생성한 answer를 로컬 디스크립션으로 설정한다.
  4. 이제 생성한 answer를 SDP 형식으로 콜러한테 전송한다.
  5. 통신을 위한 준비가 완료되었으므로 ICE를 시작한다. ICE는 Interactive Connectivity Establishment의 약자로 P2P 통신등에서 NAT등의 네트워크에서 사용되는 기술의 이름이다.

이제 다시 콜러를 보자.

// Caller

// 1. SDP_ANSWER와 받은 answer로 원격 디스크립션 설정
pc.setRemoteDescription(pc.SDP_ANSWER, new SessionDescription(answer));

// 2. ICE 시작
pc.startIce();

  1. 콜리가 전달한 answer를 받아서 peerConnection의 원격 디스크립션으로 설정한다.
  2. 모든 설정이 완료되었으므로 콜러도 ICE를 시작한다.

다음은 콜러와 콜백이 공통된 부분이다.

// 1. ICE 콜백
function iceCallback(candidate, bMore){
  if (candidate) {
    pc.processIceMessage(candidate);

    // candidate를 상대편으로 보낸다.
    //send {label: candidate.label, candidate: candidate.toSdp()}
  }
}

// 2. 전달받은 candidate 처리
var newCandidate = new IceCandidate(obj.label, obj.candidate);
pc.processIceMessage(newCandidate);

  1. 앞에서 peerConnection을 생성할 때 iceCallback이라 콜백을 등록했는데 그 구현체이다. 앞에서 ICE과정이 시작되면 이 콜백이 호출되는데 candidate(아마 P2P 통신을 하기 위해서 상대방을 찾기 위한 정보의 후보군으로 보인다.)처리하고 candidate를 객체로 만들어서 상대편으로 전달한다.
  2. 이렇게 전달된 candidate 객체를 받아서 processIceMessage로 처리해준다.


PeerConnection 예제
솔직히 이 과정까지를 이해하는데 무척 힘들었다. ㅠㅠ 서버에서 정확히 어떤 처리를 하는지가 제대로 설명되어 있는 것이 없었기 때문인데 그래서 일일이 과정을 테스트해 볼 수 밖에 없었다. 테스트한 예제소스는 Github에 올려놓았다. peerConnectionExample1는 로컬에서 peerConnection을 사용하는 예제다.(소스는 굳이 설명하지 않는다.) video 태그를 2개 두고 왼쪽의 스트림을 다른 peerConnection 객체에 전달해서 다른 <video> 태그에 보여주는 방식이다.

peerConnectionExample2은 XHR을 사용해서 단방향으로 화상을 전달하는 예제고 peerConnectionExample3는 XHR을 이용해서 양방향으로 주고 받는 예제이다.

PeerConnection

이 예제를 실행하면 위와 같은 화면이 나온다. 간단히 설명하면 콜러와 콜리 둘다 Start버튼을 눌러서 카메라/마이크의 사용을 허락하면 왼쪽 화면에 로컬 카메라의 화면이 나타난다. 그리고 콜러에서 Call 버튼을 눌러놓고 콜리에서 Join 버튼을 누르면 위처럼 서로 화상을 주고 받는 것을 확인할 수 있다.

peerConnectionExample4는 XHR대신 웹소켓을 사용한 예제이다. 앞의 예제랑 동일하지만 웹소켓이므로 Join 버튼은 필요없다. 결론부터 얘기하면 당연히 offer, answer, candidate를 주고 받는데 XHR보다는 웹소켓이 훨씬 편하다. XHR은 서버에 전송은 편하지만 서버에서 받으려면 롱폴링을 사용해야 하므로 사용하기가 그다지 편하지 않다. 굳이 XHR 예제를 만든 이유는 peerConnection의 과정을 제대로 이해할 수가 없어서 각 단계를 명확히 하면서 테스트하느라고 나온 것이다. 서버는 모두 Node.js를 사용했고 과정을 이해하기위한 예제이므로 예외처리같은 건 전혀 들어있지 않고 한번 테스트한 후에는 서버를 내렸다가 올려야 한다. 그리고 웹소켓은 websocket.io를 이용해서 구현했다.


STUN Server
앞에서 peerConnection객체를 생성할 때 STUN에 대한 얘기를 했다. 결론부터 말하면 이 부분까지는 제대로 테스트하지 못했다. STUN을 얘기하기 전에 네트워크 얘기를 약간 하면 보통 PC는 자신의 IP를 가지는데 보통 이 아이피는 공인IP가 아닌 네트워크내의 로컬 IP인 경우가 많다. 그래서 콜러와 콜리가 다른 네트워크에 있다면 서로 스트림을 주고 받기위해서는 상대를 찾을 수 있어야 하지만 이렇게 네트워크 뒤에 숨어져 있는 경우에는 상태를 찾을 수 없으므로 스트림도 주고 받을 수 없다. 그래서 앞에서 얘기한 예제는 콜러와 콜리가 같은 네트워크에 있는 경우에는 잘 동작하지만 그렇지 않으면 동작하지 않는다. ICE까지도 잘 되지만 스트림을 주고받지 못한다.

이 문제를 해결하기 위한 것이 STUN 서버이다.(STUN과 TURN이 있는데 찾아본 정보내에서는 TURN은 아직 peerConnection에서 지원하지 않는다.) STUN 서버라는 별도의 서버가 중개역할을 해서 NAT 뒤에 있는 콜러와 콜리의 공인정보를 찾아서 서로 통신할 수 있게 해주고 이 기술은 peerConnection을 위해서 만들어진게 아니고 VoIP나 P2P 통신에 원래 있던 기술이다.(이번에 처음 알아서 자세히는 모른다.)

HTML5Rocks등에 나오는 구글의 공인 STUN 서버가 있기는 하지만 결과적으로 STUN을 사용해서 다른 네트워크에서 peerConnection을 사용하는 것은 성공하지 못했다.(누가 좀 해결해서 알려줬음 좋겠다.) 구글의 STUN이 퍼블릭인것 같아서 사용가능할 것같은데 이상하게 동작을 하지 않았고 아무리 해봐도 해결을 하지 못했다. STUN은 앞에서 new webkitPeerConnection00(null, iceCallback)처럼 객체 생성시 new webkitPeerConnection00("STUN stun.l.google.com:19302", iceCallback)와 같이 STUN을 지정해서 사용한다.
2012/10/31 01:45 2012/10/31 01:45