본문 바로가기
WebRTC

WebRTC 튜토리얼

by 붕어사랑 티스토리 2022. 10. 7.
반응형

https://webrtc.org/getting-started/peer-connections

 

피어 연결 시작하기  |  WebRTC

Google은 흑인 공동체를 위한 인종적 평등을 추구하기 위해 노력하고 있습니다. 자세히 알아보기 이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 피어 연결 시작하기 피어 연

webrtc.org

 

 

Peer Connection

peer connection이란 두개의 서로다른 컴퓨터에 돌아가는 application을 p2p방식으로 연결해주는 것 이다. 피어간에 통신은 비디오, 오디오 또는 임의의 바이너리 데이터(RTCDataChannel API를 지원할 때) 가 될 수 있다.

 

두 피어가 서로를 발견하기 위해서는, 두 피어 모두 ICE Server configuration을 제공해야 한다. 이것은 STUN 또는 TURN서버가 될 수있다. 그리고 이서버들의 역할은 각각의 클라이언트들이게 ICE candidates를 제공해준다. 그리고 후보를 제공하면 리모트 피어에게 데이터를 전송한다.

 

 

Signaling

시그널링이란 p2p연결이 되기 전, 나의 Remote Client와 통신하는 과정을 말한다. WebRTC에서 시그널링에 대한 특정한 방법을 강요하지 않는다. 즉 p2p연결되기전 이 과정은 자유라는 뜻이다.

 

보통 HTTP 통신을 통해 이루어진다. 이 과정에서 두 피어는 어떻게 통신할지에 대해 정보를 나누게 된다.

 

아래 코드는 간단한 가상의 시그널링에 대한 예제이다. 앞서말했듯이 webRTC에서는 이런 시그널링에 특정한 방법을 정하지 않았고 시그널링을 구현하는데 다양한 방법이 사용될 수 있다.

// Set up an asynchronous communication channel that will be
// used during the peer connection setup
const signalingChannel = new SignalingChannel(remoteClientId);
signalingChannel.addEventListener('message', message => {
    // New message from remote client received
});

// Send an asynchronous message to the remote client
signalingChannel.send('Hello!');

(공식문서에 나온 코드인데 저 SignalingChannel은 어느 API가 아니라 따로 정의해서 만드는 함수인듯 하다)

 

 

 

 

Peer Connection 하기

피어 커넥션은 RTCPeerConnection 오브젝트를 통해 이루어진다. RTCPeerConnection의 생성자는 RTCConfiguration 오브젝트를 파리머터로 요구한다. RTCConfiguration 오브젝트는 피어 커넥션이 구성을 정의하고 반드시 어떤 ICE server를 쓸지 포함해야 된다.(라고 적혀있지만... ICE server를 넣지 않아도 로컬에서는 알아서 잘 찾아주는듯)

 

 

RTCPeerConnection이 생성되면, SDP offer 또는 answer을 만들어야 된다. 상대방 피어를 calling하는 쪽은 offer를 만들어야 하고, receiving 하는 쪽은 answer를 만들어야 한다. SDP offer 또는 answer가 만들어지면, 이 둘은 서로 다른채널을 통해 상대방 피어에게 전송되야 한다. SDP offer를 전달하는 과정을 우리가 앞서 배운 시그널링이라 하며, 이 과정은 WebRTC에 정해지지 않고 자유로운 방법으로 할 수 있다.

 

 

이제 본격적으로 피어커넥션과정을 살펴보자

 

Calling 과정

  • Calling하는쪽에서 RTCPeerConnection을 생성한다.
  • RTCPeerConnection객체를 통해 createOffer() 메소드를 호출한다
  • createOffer()메소드는 RTCSessionDescription 오브젝트를 생성한다
  • 이 세션 디스크립션은 setLocalDescription() 메소드를 통해 RTCPeerConnection 객체에 등록한다
  • 시그널링을 통해 offer(RTCSessionDescription)을 상대방 피어에게 전달한다

 

Receiving 과정

  • Receiving쪽에서도 RTCPeerConnection 객체를 생성한다
  • 시그널링을 통해 전달된 Offer(RTCSessionDescription)을 setRemoteDescription() 메소드로 등록한다
  • Remote Description이 등록되면, createAnwer() 메소드를 호출한다. 이는 전달받은 offer에 대응하는 RTCSessionDecription이 된다
  • 시그널링을 통해 answer를 calling한 피어 쪽에 전달한다
  • calling사이드는 answer를 전달 받으면 setRemoteDescription을 통해 상대방 피어를 등록한다

 

 

 

간단하게 정리 하자면

 

- 두 피어 서로 자신만에 RTCPeerConnection을 만듬

- 피어 연결을 call 하는 쪽은 createOffer로, receiving하는 쪽은 createAnswer로 Description을 만든다.

- 자신의 description은 setLocalDescription으로, 상대방의 description은 setRemoteDescription으로 등록한다.

- description전송은 시그널링을 통해 전달한다

 

 

Calling쪽 예시

async function makeCall() {
    const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
    const peerConnection = new RTCPeerConnection(configuration);
    signalingChannel.addEventListener('message', async message => {
        if (message.answer) {
            const remoteDesc = new RTCSessionDescription(message.answer);
            await peerConnection.setRemoteDescription(remoteDesc);
        }
    });
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);
    signalingChannel.send({'offer': offer});
}

Receiving쪽 예시

const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
    if (message.offer) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);
        signalingChannel.send({'answer': answer});
    }
});

 

 

자 이제 서로가 description을 받고 상대방에 대한 capabilitiy에 대한 정보를 알게 되었다. 허나 한가지 기억할 점은

서로가 상대방의description을 받았다고 해서 peer connection이 완료되었다는 점이 아니라는 것 이다!

 

커넥션을 완료하려면 우리는 ICE candidate를 모아야 하고 이를 상대방 peer에게 전달해 주어야 한다

 

 

 

ICE candidates

An icecandidate event is sent to an RTCPeerConnection when an RTCIceCandidate has been identified and added to the local peer by a call to RTCPeerConnection.setLocalDescription(). The event handler should transmit the candidate to the remote peer over the signaling channel so the remote peer can add it to its set of remote candidates.

(출저 : https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/icecandidate_event)

ICE Candidate의 발생시점은 PeerConnection에 local description이 등록될 때 발생한다.

WebRTC로 두 피어간 통신을 하기 전에, 먼저 두 피어는 서로 connectivity 정보를 교환해야 된다. 네트워크의 상황이 여러가지 요인으로 인해 달라질 수 있으므로, 제3자의 외부서비스가 보통 연결가능한 피어를 찾는데 사용된다. 이러한 서비스를 우리는 ICE 라고 부르고 STUN 또는 TURN서버를 사용한다. 

 

 

 

 

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

(대충 넘어가세요...)

STUN은 NAT용 Session Traversal Utilities의 약자로, 일반적으로 대부분의 WebRTC 애플리케이션에서 간접적으로 사용된다.

 

TURN(Traversal Using Relay NAT)은 STUN 프로토콜을 통합하고 대부분의 상용 WebRTC 기반 서비스는 TURN 서버를 사용하여 피어 간의 연결을 설정하는 고급 솔루션이다. WebRTC API는 STUN과 TURN을 직접 지원하며, 보다 완전한 인터넷 연결 설정이라는 용어로 수집된다.

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

 

WebRTC 연결을 만들 때 일반적으로 RTCPeerConnection 오브젝트에 대한 RTCConfiguration에서 하나 이상의 ICE 서버를 지정할 수 있다.

 

 

 

Trickle ICE 기법

 

RTCPeerConnection 오브젝트가 생성되면, 기본 프레임워크는 전달받은 ICE 서버들을 이용하여 ICE Candidates를 수집한다. 여기서 WebRTC는 이 과정의 status change에 대한 콜백을 제공한다. 그리고 이 콜백(또는 이벤트)의 이름은 icegatheringstatechange 이다. status 총 세가지, new gathering, complete가 있다.

 

피어가 ICE 모으는걸 끝날 떄 까지 기다릴 수 있지만, 일반적으로 보통 trickle ice라는 기법을 이용해 ice candidate가 나타나면 리모트피어에 이를 전달해주는 기법을 사용한다. 이렇게 하면 셋업 타임을 많이 단축 시킬 수 있다.

 

(한마디로 두피어 서로 열심히 ice candidate 찾다가 발견하면 서로 알려준다는 뜻인듯?)

 

 

이를 구현하려면 단순하게 icecandidate 이벤트를 추가해주면 된다. 이 RTCPeerConnectionIceEvent는 candidate를 포함하고 있고 리모트 피어에게 시그널을 통해 전달해야 한다.

 

 

아래 예시는 icecandidate를 발견하면 즉시 상대방 peer에게 전달하고, 상대방 피어가 ice 발견한걸 전달받아 addIceCandidate를 추가하는 코드이다.

// Listen for local ICE candidates on the local RTCPeerConnection
peerConnection.addEventListener('icecandidate', event => {
    if (event.candidate) {
        signalingChannel.send({'new-ice-candidate': event.candidate});
    }
});

// Listen for remote ICE candidates and add them to the local RTCPeerConnection
signalingChannel.addEventListener('message', async message => {
    if (message.iceCandidate) {
        try {
            await peerConnection.addIceCandidate(message.iceCandidate);
        } catch (e) {
            console.error('Error adding received ice candidate', e);
        }
    }
});

 

 

ICE candidate를 전달받게 되면, 우리는 비로소 peer connection이 완료되었다고 할 수 있다. 완료시점을 파악하고 싶으면 connectionstatechange라는 이벤트를 등록하여 사용하면 된다

 

// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
    if (peerConnection.connectionState === 'connected') {
        // Peers connected!
    }
});

 

 

 

 

 

 

Stream 전달하기/받기

PeerConnection이 완료되었다면 addTract 메소드를 통하여 서버에 스트림을 전달할 수 있다.

 

 

로컬에서 리모트로 스트림 전달하기

const localStream = await getUserMedia({vide: true, audio: true});
const peerConnection = new RTCPeerConnection(iceConfig);
localStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, localStream);
});

 

 

 

리모트에서 전다해주는 스트림을 받으려면 RTCPeerConnection에 track 이벤트를 추가해주면 된다. 이 RTCTrackEvent는 MediaStream 오브젝트들을 포함하고 있다. 각 MediaStream 오브젝트들은 로컬에서 보내준 MediaStream.id과 같은 값을 가지고 있다.

 

(주의 : MeidaStream.id의 값은 로컬과 리모트가 같으나, MediaStreamTrack의 ID의 경우 일반적으로 같지 않다)

 

리모트에서 온 스트림 전달받기

const remoteVideo = document.querySelector('#remoteVideo');

peerConnection.addEventListener('track', async (event) => {
    const [remoteStream] = event.streams;
    remoteVideo.srcObject = remoteStream;
});

 

 

 

 

 

 

Data Channel

WebRTC는 스트림 데이터 뿐만 아니라 임의의 데이터도 서로 교환 할 수 있다. 이는 DataChannel을 통해 이루어진다. DataChannel을 생성하는 방법은 RTCPeerConnection.createDataChannel()를 호출하는 것 이다.

 

const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel();

 

리모트 채널에서 데이터를 전달받으려면 datachannel 이벤트 콜백을 RTCPeerConnection에 등록하면 된다.

전달받은 이벤트는 RTCDataChannelEvent 오브젝트이며 channel 프로퍼티를 포함하고 있다. 이 channel프로퍼티에 전달되는 데이터가 담겨있다.

const peerConnection = new RTCPeerConnection(configuration);
peerConnection.addEventListener('datachannel', event => {
    const dataChannel = event.channel;
});

 

 

Open and Close event

당연한 얘기지만, 데이터채널이 사용되기전에, 클라이언트들은 데이터 채널이 오픈되기까지 기다려야 한다. 그리고 오픈된 시점과 채널이 종료되는 시점에 콜백을 달아주고 싶으면 open 이벤트와 close 이벤트를 추가해주면 된다

const messageBox = document.querySelector('#messageBox');
const sendButton = document.querySelector('#sendButton');
const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel();

// Enable textarea and button when opened
dataChannel.addEventListener('open', event => {
    messageBox.disabled = false;
    messageBox.focus();
    sendButton.disabled = false;
});

// Disable input when closed
dataChannel.addEventListener('close', event => {
    messageBox.disabled = false;
    sendButton.disabled = false;
});

 

 

Message

본격적으로 데이터를 보내보자. 데이터를 보내려면 dataChannel을 생성한뒤 send() 함수를 사용하면 된다.

데이터의 형태는 string, Blob, ArrayBuffer 또는 ArrayBufferView가 될 수 있따.

const incomingMessages = document.querySelector('#incomingMessages');

const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel();

// Append new messages to the box of incoming messages
dataChannel.addEventListener('message', event => {
    const message = event.data;
    incomingMessages.textContent += message + '\n';
});

 

 

 

 

 

RTCRtpSender

webRTC간에 stream에 대한 컨트롤을 할 수 있는 오브젝트이다. 만약 카메라나 오디오 장치를 바꾸게 된다면, RTCRtpSender.replaceTrack() 이라는 메소드를 이용하자

반응형

댓글