WebRTC 개념

dragonappear·2021년 11월 26일
0

WebRTC

목록 보기
1/2

목차:

  • WebRTC 란?
  • WebRTC 클래스
  • Signaling
  • RTCPeerConnection
  • RTCPeerConnection plus servers
  • 코드 분석
  • WebRTC 용어 정리
  • 참고

튜토리얼 보고 개인 기록용으로 적은거라 튜토리얼 보는게 더 나으실겁니다!

WebRTC 란?

WebRTC(web real-time commnication)는 웹 어플리케이션 간에(최근에는 android 및 ios도 지원) 플러그인(별도의 소프트웨어)의 도움 없이 서로 통신할 수 있도록 설계된 API이다. 웹 브라우저 기반의 통신 방식이다. W3C에서 제시된 초안이며, 음성 통화, 영상 통화, P2P 파일 공유 등으로 활용될 수 있다. [출처: 위키백과]

설계

WebRTC의 주요 구성 요소는 여러 자바스크립트 API를 포함하고 있다.

  • getUserMedia : 오디오와 비디오 미디어를 가져온다.(예: 장치의 카메라와 마이크로폰에 접근하여)
  • RTCPeerConnection: 피어 간 오디오, 비디오 통신을 활성화한다. 신호 처리, 코덱 관리, P2P통신,보안,대역폭 관리를 수행한다.
  • RTCDataChannel: 피어 간 양방향 임의 데이터 통신을 허용한다. 웹소켓과 동일한 API를 사용한다.

WebRTC 클래스

P2P : Peer to Peer의 줄임말로 중앙 서버를 거치지 않고 클라이언트 컴퓨터끼리 직접 통신하는 방식을 통칭한다. 일반적으로 컴퓨터의 쌍방향 파일 전송 시스템을 가리키지만 P2P라고 해서 꼭 파일 전송에 쓰이는 것은 아니다. 중앙서버 없이 컴퓨터와 컴퓨터간을 연결해 주며, 프로그램 엔진만 만들면 되기 때문에 매우 간편하게 파일을 전송할 수 있는 장점이 있다. 처음에는 느린 인터넷 속도 때문에 잘 사용되지 않았으나 인터넷이 점차 발달할수록 파일전송속도가 빨라짐에 따라 굉장히 널리 사용되게 되었다.(출처:https://namu.wiki/w/P2P)

  • WebRTC 기술은 P2P 통신에 최적화 되어있다.

  • WebRTC에 사용되는 기술은 여러가지가 있지만 크게 위에서 말한 3가지 클래스에 의해서 실시간 데이터 교환이 일어난다.

    • MediaStream (aka getUserMedia) : 카메라/마이크 등 데이터 스트림 접근
    • RTCPeerConnection: 암호화 및 대역폭 관리 및 오디오 또는 비디오 연결
    • RTCDataChannel: 일반적인 데이터 P2P 통신
  • 위 3가지 객체를 통해서 데이터 교환이 이루어지며, RTCPeerConnection들이 적절하게 데이터를 교환할 수 있게 처리하는 과정을 시그널링(Signaling)이라고 한다.

  • PeerConnection은 두 명의 유저가 스트림을 주고 받는 것이므로 연결을 요청한 콜러(caller)와 연결을 받는 콜리(callee)가 존재한다.

  • 콜러와 콜리가 통신을 하기 위해서는 중간 역할을 하는 서버가 필요하고 서버를 통해서 SessionDescription을 서로 주고 받아야 한다.


Signaling: session control, network and media information

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

대신, WebRTC 개발자들은 SIP,XMPP 또는 적절한 쌍방통신 채널 등 자신들에게 편한 방식을 선택할 수 있습니다. appr.tc 예제는 Signaling 방식으로 XHR과 Channel API를 사용하였습니다. codelab은 Node 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에서는 RTCPeerConnectionprefix를 가지고 있다는 점을 유의해야합니다.

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가 serializedcandidate data를 WebSocket 또는 어떤 특정 방식 등 그들만의 Signaling 채널을 통해 Bob에게 전송한다.
  4. Bob이 Alice로부터 candidate 메시지를 받으면 Bob은 peer description을 위한 candidate를 추가하기 위해 addIceCandidate를 호출한다.

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

  1. 엘리스가 RTCPeerConnectioncreateOffer() 함수를 호출한다. 이 함수의 인자로 입력된 callback을 통해 엘리스의 Session description 정보를 나타내는 RTCSessionDescription을 얻는다.
  2. callback안에서 엘리스는 자신의 descriptionsetLocalDescription() 함수로 설정하고 이 session descriptionsignaling 채널을 통해 Bob에게 전달한다.(setLocalDescription()가 호출되기 전까지는 candidates 수집이 시작되지 않는 다는 점을 주의해야 합니다)
  3. Bob은 Alice가 보낸 session description 정보를 setRemoteDescription() 함수를 통해 설정합니다.
  4. Bob은 RTCPeerConnectioncreateAnswer() 함수에 Alice로 부터 받은 remote description을 전달하고 실행합니다. 그러면 그녀의 정보를 기반으로 Local Session이 생성됩니다. createAnswer()callbackRTCSessionDescription을 전달해줍니다(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

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


출처:https://www.html5rocks.com/ko/tutorials/webrtc/basics/

Signaling 과정이 성공적으로 마치면 data는 peer와 peer 끼리 또는 송신자와 수신자가 직접 주고 받게 된다.
(스트리밍이 RTCPeerConnection의 역할이다.)


RTCPeerConnection

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

아래그림은 RTCPeerConnection의 역할을 보여주는 WebRTC 아키텍쳐 다이어그램이다. 졸라 복잡!


출처:https://www.html5rocks.com/ko/tutorials/webrtc/basics/

RTCPeerConnection 뒤에 숨겨진 많은 것들이 WebRTC에서 사용되는 코덱들과 프로토콜들은 불안정한 네트워크 위에서도 실시간 통신이 가능하게 하기위해 엄청난 양의 일들을 하고 있다는 것만 알고 넘어가면 될듯하다.


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 framework: 비디오 채팅 클라이언트 같은 피어들을 연결하기위한 프레임워크입니다. 우선, ICE는 가장 대기시간이 적은 UDP를 통해 피어들 끼리 directly 연결 가능한지 시도합니다.여기에서 STUN 서버들은 한가지일을 합니다: NAT 뒤에 있는 피어들이 연결 가능하도록 자신들의 공용(public)주소와 포트를 찾아줍니다.


코드 분석

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 EngineChannel API를 사용합니다.


출처:https://www.html5rocks.com/ko/tutorials/webrtc/basics/

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


출처:https://www.html5rocks.com/ko/tutorials/webrtc/basics/

메세지 전송하는 방법은 다음과 같다.

  1. 클라이언트 B는 변경사항과 함께 App Engine 앱으로 POST 요청을 보낸다.
  2. App Engine 앱은 요청을 channel로 전달한다.
  3. 채널은 메세지를 클라이언트 A에게 전달한다.
  4. 클라이언트 A의 onmessage 콜백함수가 호출된다.


출처:https://www.html5rocks.com/ko/tutorials/webrtc/basics/

다시 말하지만: 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를 위해 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는 서로를 발견하고 자신들의 기능에 대한 정보를 교환하고, 통화 세션을 초기화합니다. 그러면 실시간 데이터 통신이 시작될 수 있습니다.


WebRTC 용어 정리

출처

Stun Server , Turn Server

WebRTC는 P2P에 최적화되어있다. 즉 Peer들간의 공인 네트워크 주소(ip)를 알아야 데이터 교환을 할 수 있는데, 실제 개개인의 컴퓨터는 방화벽등 여러가지 보호장치들이 존재한다.
그래서 Peer들간의 연결이 쉽지 않은데, 이렇게 서로 간의 연결을 위한 정보를 공유하여 P2P 통신을 가능하게 해주는 것이 Stun/Turn Server이다. 더 자세한 내용은 링크로

SDP (Session Description Protocol)

세션 기술 프로토콜은 스트리밍 미디어의 초기화 argument를 기술하기 위한 포맷이다. 실제로 WEB RTC는 SDP format 에 맞춰져 영상,음성 데이터를 교환하고 있다.

ICE (Interactive Connectivity Establishment)

NAT환경에서 자신의 Public IP를 파악하고 상대방에게 데이터를 전송하기 위한 Peer간의 응답 프로토콜로 일반적으로 STUN/TURN을 이용해서 구축을 한다.
간단하게 설명하면, 한쪽이 Offer를 보내면 다른 한쪽이 Answer함으로써 피어간 연결이 설정된다


참고:

0개의 댓글