Skip to content

3주차 WebRTC로직

JeongHyeonJo edited this page Nov 22, 2019 · 1 revision

Week3 기술공유

webRTC 연결과정과 rtcPeerConnection 최소화

Nov 21, 2019 조정현

주제 선정 이유

webRTC(Web Real-TIme Communications)는 p2p 화상 통화나 데이터 전송을 할 수 있도록 돕는 기술입니다. 해당 기술을 선정한 이유는 p2p 스트리밍으로 서버의 과부하를 줄이고, 별도의 프로그램 설치 없이 소비자들의 상호 연결을 하는 데에 webRTC가 적절한 기술이라 판단했기 때문입니다.

webRTC의 기반 기술

webRTC는 크게 Web API와 내부 엔진으로 구성되며 다음과 같은 구조를 가집니다.

webrtc_architecture

브라우저에서 사용하는 API는 내부 C++ API와 연결되며, webRTC 내부는 Voice Engine, Video Engine,
그리고 네트워크 연결을 담당하는 Transport의 총 3가지 부분에서 역할을 분담합니다.

Voice Engine은 음성 stream에 관한 처리를 하는 부분입니다. 오디오의 코덱을 제공해주며, 소리가 울리는 것을 방지하거나 잡음이 들리지 않도록 해줍니다.

Video Engine은 영상의 노이즈를 제거해주고, 구글 제공의 VP8 영상 코덱(오픈 소프트웨어)을 통해 영상을 송출할 수 있게 합니다.

마지막으로 가장 중요한 Transport에서는 UDP 기반의 Secure Real-Time Transport (SRTP) 프로토콜(AES 암호화)을 사용해 보안을 유지시키며, 오디오와 비디오 간의 싱크를 맞춰줍니다. 해당 프로토콜을 통해 영상 스트리밍이 진행되며, 스트리밍 자체가 암호화된 값으로 전달되기에 이를 복호화할 키값을 상대방이 가지고 있어야 합니다.

이에 더해 Transport는 Datagram Transport Layer Security (DTLS) , Stream Control Transport Protocol (SCTP) 를 통해 App에서 필요한 데이터를 전송할 수 있게 합니다.

webRTC의 연결 과정

webRTC는 크게 Session Description Protocol (SDP) 교환 과정, iceCandidate 교환 과정을 거칩니다.

SDP에는 SRTP의 암호키와 대역폭, 영상의 크기 등 영상을 송출하기 위한 기본 설정이 들어있습니다. 해당 과정을 거치지 않으면 스트리밍된 영상을 복호화할 수 없기에 가장 먼저 생성하는 과정을 거쳤습니다.

SDP를 교환하는 코드는 다음과 같습니다.

(소켓 통신을 통해 서버와 ClientA, ClientB 모두 서로의 socketId를 전달받았다고 가정합니다.
또한 ClientA와 ClientB는 모두 서로에게 대응하는 rtcPeerConnection 객체를 만들었다고 가정합니다.

ClientA - local description 송출

//송출자는 createOffer을 통해 offer description을 만듭니다.
const description = await rtcPeerConnection.createOffer();
await connection.setLocalDescription(description);
socket.emit('sendDescription', {target: ClientB.socketId, description});

Server - local description 전송

socket.on('sendDescription', ({ target, description }) => {
  io.to(target).emit('sendDescription', {
    //수신자가 송출자에게 반송할 수 있도록 target을 변경해줍니다.
    target: socket.id,
    description,
  });
});

ClientB - remote description 수신

rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(clientADescription));
//clientA에게 보낼 answer description을 만듭니다.
const description = await connection.createAnswer();
await connection.setLocalDescription(description);
socket.emit('sendDescription', {target: ClientB.socketId, description});

ClientA - remote description 수신

rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(clientBDescription));
//answer description을 받았기에 더이상 송신을 하지 않습니다.

이 상태에서 다음 단계로 ice candidate를 교환하도록 했습니다.

ice candidate는 webRTC에서 Client 사이를 연결하는 최단 경로로,
다수의 후보 경로가 탐색될 수 있습니다.

client끼리 서로 iceCandidate를 탐색하고, 교환하면서 최적의 경로를 찾아가면 됩니다.

ice candidate를 발견하고, 공유하는 코드는 다음과 같습니다.

Clients

//만약 ice candidate를 찾았다면, 해당 candidate를 상대방에게 보냅니다.
const iceCandidateHandler = (event) => {
    socket.emit('sendCandidate', {
        target: opponentSocketId,
        candidate: event.candidate,
    })
}

connection.onicecandidate = iceCandidateHandler;

//candidate를 상대편에서 받았을 경우, 해당 candidate를 자신의 connection에 추가합니다.
async sendCandidateHandler = ({ target, candidate }) => {
  const rtcIceCandidate = new RTCIceCandidate(candidate);
  await rtcPeerConnection.addIceCandidate(rtcIceCandidate);
}

socket.on('sendCandidate', sendCandidateHandler);

Server

socket.on('sendCandidate', ({ target, candidate }) => {
  io.to(target).emit('sendCandidate', {
    target: socket.id,
    candidate,
  });
});

이러한 과정을 통해 서로간의 연결이 성립되고, rtcPeerConnection의 ontrack 이벤트의 stream을 비디오와 연결해주면 화면을 전달받을 수 있습니다.

registerOnTrackEvent(socketId) {
  rtcPeerConnection.ontrack = event => {
    //streamerVideo는 화면 송출용 video element입니다.
    [streamerVideo.srcObject] = event.streams;
  };
}

rtcPeerConnection의 최소화

week2까지는 위에서 말한 방식을 활용해 mesh topology 방식으로 영상을 송출했습니다.

이를 그림으로 표현하면 다음과 같이 표현할 수 있습니다.

mesh_topology

이 모델은 구현하기 간단하다는 장점이 있지만 모든 Client들이 자신을 제외한 Client의 수만큼 상대의 rtcPeerConnection 객체를 가지고 있어야 한다는 단점이 있습니다.

제출자가 하나뿐인 우리 프로젝트의 특성상 영상을 송출하는 사람은 한명만 있어도 충분했기 때문에 새롭게 구성한 망 구조는 다음과 같았습니다.

new_topology

Viewer 사이에 존재하던 연결이 사라졌기 때문에 Viewer가 관리하는 rtcPeerConnection 객체가 하나로 줄어들고, Streamer가 Viewer 전체의 rtcPeerConnection을 관리하게 됩니다.

이 과정에서 streamer는 connection.createOffer을 통해 offer description을 만들고, viewer는 connection.createAnswer()을 통해 offer에 대한 answer를 만들게 됩니다.

코드는 다음과 같습니다.

async createDescription(offerOrAnswer, socketId) {
  const connection = rtcPeerConnections[socketId];
  const description =
    //외부의 분기 코드(서버에서 streamer 여부인지 socket을 통해 전달)를 통해
    //streamer인 경우에는 offerOrAnswer가 'answer'로,
    //viewer인 경우에는 'offer'를 받게 됩니다.
    offerOrAnswer === 'offer'
      ? await connection.createOffer()
      : await connection.createAnswer();
  await connection.setLocalDescription(description);
  socket.emit('sendDescription', {
    target: socketId,
    description,
  });
}

async sendDescriptionHandler({ target, description }) {
  await rtcPeerConnections[target].setRemoteDescription(
    new RTCSessionDescription(description),
  );
  if (description.type === 'answer') return;
  await createDescription('answer', target);
}

socket.on('sendDescription', sendDescriptionHandler);

이를 통해 개인이 streamer인지, viewer인지 여부에 따라
화면을 송출하는 것을 설정할 수 있게 했습니다.

다음주에 해야 할 일

현재 망 구조를 변경했지만 streamer가 전환되는 이벤트를 구현하지 못했습니다.

이를 추가해서 게임 시스템에 따라 시간이 지나면
streamer(문제 출제자)가 변경되는 기능을 구현하고

채팅 시스템을 추가해서 게임을 진행할 수 있도록 만드는 것을
이후의 해야 할 일로 생각하고 있습니다.

Clone this wiki locally