WebRTC를 React에서 구현해보자!

김정현·2023년 1월 18일
1

Develop

목록 보기
7/7
post-thumbnail

현재 WebRTC를 이용한 화상회의 프로젝트를 개발중입니다.
해당 프로젝트에서 사용한 WebRTC의 개념과 1:1 통신을 위한 기본적인 연결 과정에 대해 포스팅하고자 합니다.

🖥️ WebRTC란?

Web-RealTime-Communication 의 약자로 서버의 개입이나 플러그인 없이 브라우저간 통신을 할 수 있게 하는 기술입니다.

W3C에서 제시된 초안이며, 음성 통화, 영상 통화, P2P 파일 공유 등으로 활용될 수 있습니다.

현재 유일한 P2P 표준이기도 합니다.

WebRTC의 특징

브라우저간 통신을 한다는 독특한 기술적 특징 때문에 다른 스트림 서비스와 달리 3가지의 서버가 필요합니다.

이 서버들은 데이터 전송에는 직접적인 역할을 하지 않으며 브라우저간 통신을 위해 필요한 사전 데이터를 주고받는 용도로 사용됩니다.

이번 포스팅에선 그 중 가장 기본적인 시그널링 서버에 대해 설명한 후 연결 과정에 대해 설명하겠습니다.

Signaling Server

브라우저간 통신을 위해선 우선 서로의 정보를 알아야 합니다. 이를 중계해주는 서버를 signaling server 라고 합니다.

각 브라우저는 signaling server를 통해 자신의 정보를 전달하고 상대방의 정보를 받아 P2P 연결을 하게 됩니다.

서로의 정보를 알기 위해 필요한 데이터 송수신 과정은 다음 그림과 같습니다.

💡 1 : 1 통신을 예제로 설명합니다.

방장이 화상회의에 필요한 방을 만든 후 참여자가 입장하는 상황을 설정해보겠습니다.

  1. 방장은 자신의 미디어 정보를 stream에 추가합니다.
  2. 방장은 offer를 생성합니다. offer에는 방장의 미디어 정보를 담고 있는 sdp 정보가 담겨 있습니다.
  3. 방장은 생성한 offer를 자신의 localDescription으로 설정합니다.
  4. 방장은 자신의 offer 정보를 서버로 전송합니다.
  5. 참여자는 서버로부터 받은 방장의 offer 정보를 remoteDescription으로 설정합니다.
  6. 참여자는 자신의 미디어 정보를 stream에 추가합니다.
  7. 참여자는 answer을 생성합니다. answer은 참여자의 미디어 정보를 담고 있는 sdp 정보가 담겨 있습니다.
  8. 참여자는 answer를 자신의 localDescription으로 설정합니다.
  9. 참여자는 자신의 answer 정보를 서버로 전송합니다.
  10. 방장은 서버로부터 받은 참여자의 answer 정보를 remoteDescription으로 설정합니다.
  11. 방장과 참여자는 서버로부터 받은 candidate 등록합니다.
  12. 통신을 위한 기본 설정이 끝납니다.

중간에 ICECandidate 이벤트 리스너 설정 등의 추가 작업이 있지만 해당 내용은 아래 내용으로 이어서 진행하겠습니다.

🔎 React에서 구현하기

서버에게 클라이언트의 정보를 실시간으로 전달하기 위해 websocket을 활용해야 했고 본 예제에서는 프로젝트에서 사용한 socket.io 라이브러리를 활용하여 작성했습니다.

본 프로젝트에선 webRTC 관련 함수들을 클래스로 구현하여 사용했습니다.

import { Socket } from "socket.io-client";
import { events } from "../constants/events";

export interface offerType {
  sdp: string;
  roomCode: number;
  sid: string;
}

export class WebRTC {
  peerConnection: RTCPeerConnection;
  socket: Socket;
  roomCode: number;
  constructor(socket: Socket, roomCode: number) {
    this.peerConnection = new RTCPeerConnection();
    this.socket = socket;
    this.roomCode = roomCode;
  }

  addTracks(streams: MediaStream[]) {
    streams.forEach((stream) => {
      stream
        .getTracks()
        .forEach((track) => this.peerConnection.addTrack(track, stream));
    });
  }

  async setLocalOffer(sid: string) {
    const offer = await this.peerConnection.createOffer();
    this.peerConnection.setLocalDescription(offer);

    this.socket.emit(events.OFFER, {
      sdp: offer.sdp,
      roomCode: this.roomCode,
      sid,
    });
  }

  async setRemoteOffer(offer: RTCSessionDescriptionInit, sid: string) {
    this.peerConnection.setRemoteDescription(offer);
    const answer = await this.peerConnection.createAnswer();

    this.peerConnection.setLocalDescription(answer);
    this.socket.emit(events.ANSWER, {
      sdp: answer.sdp,
      roomCode: this.roomCode,
      sid,
    });
  }

  async setAnswer(answer: RTCSessionDescriptionInit) {
    this.peerConnection.setRemoteDescription(answer);
  }

  setIceCandidate(sid: string) {
    this.peerConnection.addEventListener("icecandidate", (data) => {
      this.socket.emit("icecandidate", {
        candidate: data.candidate,
        sid: sid,
        roomCode: this.roomCode,
      });
    });
    this.socket.on(events.ICE_CANDIDATE, (response) => {
      if (response.candidate) {
        this.peerConnection.addIceCandidate(response.candidate);
      }
    });
  }

  setRemoteStream(remoteVideo: HTMLVideoElement | null) {
    this.peerConnection.addEventListener("track", async (data) => {
      if (data.track.kind === "video") {
        if (!remoteVideo) return;
        remoteVideo.srcObject = data.streams[0];
      }
    });
  }
}

0. 사전 작업

방장과 참여자 모두 방에 처음 입장하면 자신의 미디어 정보들을 MediaTrack에 넣어줘야 합니다. 그래야 해당 트랙 정보들을 바탕응로 미디어 스트림 송수신을 진행하기 때문입니다.

그 후 상대방의 MediaTrack이 추가 되었을 경우의 이벤트 리스너를 달아 줍니다.

webRTC.addTrack(myTracks);
webRTC.setRemoteStream(video)

1. 방장 이벤트 설정

방을 생성한 방장은 참여자의 입장 여부를 듣는 이벤트 리스너를 생성합니다.

해당 이벤트 리스너는 참여자가 방에 입장 시 방 참여를 확인했다는 이벤트를 전송한 후 ICECandidate 설정을 진행하며 위의 2, 3, 4을 실행합니다.

    socket.on(events.JOIN, async (response) => {
      socket.emit(events.HAND_SHAKE, {
        sid: response.sid,
        roomCode: props.roomCode,
      });
      webRTC.current.setIceCandidate(response.sid);

      webRTC.current.setLocalOffer(response.sid);
    });

2. 참여자 이벤트 설정

참여자는 방 입장 시 입장 관련 이벤트를 서버에 전송 후, 방장으로 부터 받은 참여 확인 이벤트를 통해 참여자의 ICECandidate를 설정합니다.

    socket.emit(events.JOIN, {
      nickName: props.nickName,
      roomCode: String(props.roomCode),
    });

    socket.on(events.HAND_SHAKE, (response) => {
      webRTC.setIceCandidate(response.sid);
    });

3. 참여자 answer event 설정

방 참여 후 방장으로부터 offer 이벤트 수신 시 해당 offer를 RemoteDescription으로 설정한 후 answer를 생성하여 전송하는 5,7,8,9 과정을 진행합니다.

    socket.on(events.OFFER, (response: offerType) => {
      webRTC.setRemoteOffer(
        {
          type: events.OFFER as RTCSdpType,
          sdp: response.sdp,
        },
        response.sid
      );
    });

4. 방장 answer 정보 등록

방장은 참여자로부터 받은 answer 정보를 자신의 remoteDescription으로 설정하는 10번 과정을 진행합니다.

    socket.on(events.ANSWER, (response : answerType) => {
      webRTC.setAnswer({
        type: events.ANSWER as RTCSdpType,
        sdp: response.sdp,
      });
    });
  };
profile
프론트엔드 개발자(가 되고싶은 주니어)

0개의 댓글