WebRTC 화상 연결 하기

후훗♫·2020년 5월 19일
4

바닐라코딩 부트캠프가 끝이 났다...
마지막 개인 프로젝트였던 2번째 프로젝트 peer는 누구나 쉽게 화상 상담을 연결할 수 있는 웹서비스이다.
(홍보 아님...ㅋㅋㅋ)

2번째 프로젝트의 키워드(혹은 위기...)는 2가지이다.
1) 화상 연결
2) 음성 녹음

그 중 화상 연결은 WebRTC라는 웹 API로 구현했다.
나중에 잊어버리지 않도록 기록용으로 정리한다.

1. WebRTC란?

출처: MDN

WebRTC(Web Real-Time Communications)란, 웹 어플리케이션(최근에는 android 및 ios도 지원) 및 사이트들이 별도의 소프트웨어 없이 음성, 영상 미디어 혹은 텍스트, 파일 같은 데이터를 브라우저끼리 주고 받을 수 있게 만든 기술이다. WebRTC로 구성된 프로그램들은 별도의 플러그인이나 소프트웨어 없이 p2p 화상회의 및 데이터 공유를 한다.

MDN의 설명대로 WebRTC API를 활용하면
별도의 플러그인 or소프트웨어 없이 P2P 화상회의가 가능한 프로그램을 만들 수 있다!

WebRTC는 Google Chrome팀이 브라우저나 모바일 환경에서 편리하게
Real Time Communication(RTC)을 편리하게 할 수 있는 api를 만들기 위해 시작한 프로젝트다.
구글이 주도하지만, Safari, Firefox, Opera 등 왠만하면 브라우저 최신버전에서도 사용이 가능하다.
(https://caniuse.com/)

WebRTC의 주요 API는 크게 3가지이다.

  • MediaStream - 카메라/마이크 등 데이터 스트림 접근
  • RTCPeerConnection - 암호화 및 대역폭 관리, 오디오 또는 비디오 연결
  • RTCDataChannel - json/text 데이터들을 주고받는 채널을 추상화한 API

MediaStream으로 peer(WebRTC의 Client)의 스트림을 얻고,
RTCPeerConnection으로 연결하고자 하는 peer의 정보를 주고 받아 연결된다.
이 과정을 Signaling이라고 한다.

2. Simple peer

나는 simple peer라는 라이브러리를 사용했다.
WebRTC 관련 자료를 구글링했을 때 발견했던 tutorial이 simple peer를 적용했고,
WebRTC를 그대로 사용하기에는 이해하기가 어려웠다.

peer.js라는 라이브러리도 있는데,
npm trend를 보면 최근 6개월 간 simple peer가 WebRTC 관련 라이브러리 중 다운로드 수가 가장 높다.
또 업데이트도 최근이었다.
(블로그를 쓰는 현재(5/19) 기준 일주일전에 업데이트되었다.)

3. 구현하기

개인적으로 화상 기능을 구현하면서 어렸웠던 것은
(새로 접한 개념도 물론 어려웠지만)
peer들이 연결되기 위해 주고 받는 정보들의 흐름을 이해하고 컨트롤하는 것이었다.
그래서 내가 이해한 peer 간의 signal, stream의 흐름대로 정리하려고 한다.
(실제 코드는 너무 복잡해서 최대한 간단하게 수정했다..)
(실제 코드는 github에서 확인해주세요..)

(흠.. 정리하고 다시 봐도.. 복잡한 것 같다..🥺)

시작에 앞서 내 프로젝트의 구조는 다음과 같다.

  • Consulting Bot(Caller) ➝ peer
  • Client(Callee) ➝ peer
  • Server

Consulting Bot, Client, Server 3개의 디렉토리가 있고,
Consulting Bot이 화상 요청을 하고, Client는 이에 응답한다.
(Consulting Bot에서 화상 요청이 많을 경우 순차적으로 대기한다.)
서버와 소켓으로 두 peer 간의 정보를 전달한다.

WebRTC랑은 상관없는 내용이지만, Consulting Bot은 CDN으로 제공한다.
그래서 프로젝트 기간 동안 3개의 디렉토리로 나눠 작업했다..😎

아래부터는 Caller, Callee, Sever로 언급하며 정리할 예정이다.

1) Caller의 요청

① Caller의 Video Stream을 얻어 video tag에 연결한다.

//in consulting bot
const callerStream = await navigator.mediaDevices.getUserMedia({
  video: true, audio: true
}); //만약 음성 연결을 원한다면 video는 false로 설정한다.
/* caller 브라우저 화면의 caller video tag*/.srcObject = callerStream;

② Caller의 signaling data를 얻어 Socket을 통해 Server로 전송한다.

//in consulting bot
const callerPeer = new Peer({
  initiator: true, //요청자 이므로 true!
  stream: callerStream,
});

const callerSocket = io(/* 소켓 서버*/);
callerPeer.on('signal', callerSignal => {
  //signaling data와 함께 Caller의 정보도 함께 전송
  //unique한 값인 name은 만들어진 socket room으로 활용
  //callee id로 어떤 callee와 연결하고 싶은지 구분함
  callerSocket.emit('joinCaller', { signal: callerSignal, name: callerName, callee: calleeId });
});

2) Caller의 정보 전달 (To.Server)

③ Server로 전송된 Caller의 정보를 rooms 객체에 저장한다.

//in server
const rooms = {};

serverSocket.on('joinCaller', (callerInfo) => {
  const { signal, name, callee } = callerInfo;
  const newCaller = {
    id: serverSocket.id,
    signal,
    room: name,
    callee,
  };
  
  //rooms 객체는 collee를 키 값으로 caller를 value 값으로 저장한다.
  //caller는 여러 명이 될 수 있으므로 배열의 형태로 저장한다.
  rooms[callee].length
    ? rooms[callee].push(newCaller)
    : rooms[callee] = [newCaller];
});

④ Callee가 요청에 응답했을 때 Caller 정보를 전달 받을 수 있는 Socket Event를 만든다.

//in server
serverSocket.on('startConsulting', (calleeId, callback) => {
  //전달된 calleeId를 통해 화상을 요청한 Caller의 정보를 rooms 객체에서 찾는다.
  //현재 예시 기준이라면 callerInfo는 3번에 만든 newCaller 정보이다.
  const callerInfo = findCallerInfo(calleeId);
  callback({ callerInfo });
});

3) Callee의 응답

⑤ Callee가 Caller의 요청에 응답하면 Socket을 통해 Caller의 정보를 전달받는다.

//in client
const calleeSocket = io(/* 소켓 서버*/);
calleeSocket.emit('startConsulting', (callerInfo) => {
  //callerInfo 값이 Caller의 정보(signaling data, socket id)이다.
  //해당 정보는 아래 단계에서 사용될 예정이다.
});

⑥ Callee의 비디오 Stream을 얻어 video tag에 연결한다.

//in client
calleeSocket.emit('startConsulting', (callerInfo) => {
  // 5번 코드..
  
  const calleeStream = await navigator.mediaDevices.getUserMedia({
    video: true, audio: true
  });
  /* callee 브라우저 화면의 callee video tag*/.srcObject = calleeStream;
});

⑦ Callee의 signaling data를 얻어 Socket을 통해 Server로 전송한다.

  • 전달받은 Caller의 signaling data를 Callee와 연결한다.
  • 전달받은 Caller의 stream을 video tag와 연결한다.
//in client
calleeSocket.emit('startConsulting', (callerInfo) => {
  //5번, 6번 코드..
  
  const calleePeer = new Peer({
    initiator: false, //요청자가 아니므로 false
    stream: calleeStream,
  });
  
  calleePeer.on('signal', calleeSignal => {
    //서버로 callee의 signaling data를 전송할 때, 전달받은 callerInfo의 id를 함께 전송한다.
    //id는 Caller의 Socket id이다.
    calleeSocket.emit('acceptCaller', { signal: calleeSignal, to: callerInfo.id })
  });

  calleePeer.signal(callerInfo.signal); //Caller-Callee의 연결
  
  calleePeer.on('stream', callerStream => {
    /* callee 브라우저 화면의 callee video tag*/.srcObject = callerStream;
  });
});

4) Callee의 정보 전달 (To.Server)

⑧ 응답한 Callee의 정보가 Caller에게 전달된다.

//in server
serverSocket.on('acceptCaller', (calleeInfo) => {
  io.to(calleeInfo.to).emit('acceptCallee', calleeInfo.signal);
});

5) Caller - Callee의 최종 Connection

⑨ 전달받은 Callee의 signaling data를 Caller와 연결한다.

  • 전달받은 Callee의 stream을 vidoe tag와 연결한다.
//in consulting bot
callerSocket.on('acceptCallee', (calleeSignal) => {
  callerPeer.signal(calleeSignal);
});

callerPeer.on('stream', calleeStream => {
  /* caller 브라우저 화면의 callee video tag*/.srcObject = calleeStream;
});

4. 참고자료

profile
꾸준히, 끄적끄적 해볼게요 :)

2개의 댓글

comment-user-thumbnail
2023년 12월 19일

node와 vue사용하신건가요?

1개의 답글