개발 환경
- Server : node.js
- Client: node.js, webpack을 이용한 개발 서버
아래의 깃헙을 클론 받아 초기세팅 폴더 안에 들어가면 실습할 수 있습니다.
https://github.com/kimjuno97/Implementing-Video-Chat-Using-the-WebRTC-API
사전 지식
- socket.io로 라이브 채팅 로직 구현하기 <= 선행으로 보면 도움이 될 것입니다.
Chater 1. WebRTC란?
WebRTC(Web Real-Time Communication)은 웹 애플리케이션과 사이트가 중간자 없이 브라우저 간에 오디오나 영상 미디어를 포착하고 마음대로 스트림 할 뿐 아니라, 임의의 데이터도 교환할 수 있도록 하는 기술입니다. WebRTC를 구성하는 일련의 표준들은 플러그인이나 제 3자 소프트웨어 설치 없이 종단 간 데이터 공유와 화상 회의를 가능하게 합니다.
이를 위하여 WebRTC는 상호 연관된 API와 프로토콜로 구성되어 함께 작동합니다. 이 문서에서는 WebRTC의 기본을 이해하고, 설정하며, 데이터와 미디어 연결을 위해 사용할 수 있게 도와줄 것입니다.
WebRTC에서 제공하는 api 중 RTCPeerConnection api를 사용하여 화상 채팅을 구현할 것입니다.
Chater 2. Connection Flow의 이해
Peer는 하나의 브라우저입니다.
서로 다른 클라이언트 Peer A와 Peer B는 아래의 메서드 실행 절차를 통해 연결을 시도할 것입니다.
Server는 중간에서 websocket프로토콜로 연결을 돋는 역할을 할 것입니다.

다음 chater부터는 해당 과정을 끊어서 설명하겠습니다.
코드 블럭 상위의 backend, front 문자를 보고 해당 파일에 따라 치면서 진행하면 됩니다.
Chater 3. 차근차근 로직 구현해 보기
step 1. MediaDevices.getUserMedia()
⚠️ 해당 메서드는 navigator 안에 있습니다. 그리고 localhost or https로만 요청이 올 때 respone을 반환합니다.
개발 서버의 ip주소로 접근하면 http로 요청이 가게 되는데 이때는 undefined를 반환합니다.
만약 개발서버에서 서로 다른 컴퓨터로 테스트를 하고 싶으면,
여기서 <= 해당 문제를 해결할 방법을 얻을 수 있습니다.
front
/** 본인 비디오 연결 */
async function getMedia() {
try {
myStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
});
myFace.srcObject = myStream;
} catch (err) {
console.error('getMedia 과정에서 에러가 났습니다. \n', err);
}
}
getMedia();
위처럼 간단하게 카메라를 킬 수 있다. 공식문서에서 video 옵션에 대해 다양한 옵션을 확인할 수 있다.
step 2. RTCPeerConnection
- 간단한 설명
RTCPeerConnection API는 로컬 컴퓨터와 원격 피어 간의 WebRTC 연결을 나타냅니다. 원격 피어에 연결하고, 연결을 유지 및 모니터링하고, 더 이상 필요하지 않으면 연결을 닫는 방법을 제공합니다.
RTCPeerConnection()로 연결을 위한 초기화 작업을 합니다.
Peer A 초기화라고 하겠습니다.
front
/** Peer A 초기화 */
const myPeerConnection = new RTCPeerConnection();
step 3. addStrem와 icecandidate
addStream은 stream이 추가될 때, icecandidate는 RTCPeerConnection으로 생성된 객체에 setLocalDescription이 실행되면,
일어난다.
getTracks, addTrack을 호출하여 addEventListener로 추가한 이벤트가 일어납니다.
front
function settingPeerConnection() {
myPeerConnection.addEventListener('addstream', data => {
console.log('addstream', data);
});
myPeerConnection.addEventListener('icecandidate', data => {
console.log('icecandidate', data);
});
myStream
.getTracks()
.forEach(track => myPeerConnection.addTrack(track, myStream));
}
중간 코드 수정
getMedia는 비동기 함수가 포함되어 있어서 myStream에 데이터를 받으려면,
아래와 같이 한 번에 묶어서 호출해야 합니다.
중간에 호출했던 getMedia 부분만 지워주시고, 아래의 코드를 하단에 적으면 됩니다.
front
// 비동기 제어
async function getMediaAndConnection() {
await getMedia();
settingPeerConnection();
}
getMediaAndConnection();
step 4. Peer A, Peer B 연결
front
// 웹브라우저에 먼저 들어온 사람이 Peer A가 됩니다. 채널은 임의로 1번으로 고정하겠습니다.
socket.emit('join', 1);
// 서버에서 사람이 들어오면 Peer A에 알립니다.
// 이로 인해 Peer A가 합류를 요청(offer) 합니다.
socket.on('comeHere', async () => {
const offer = await myPeerConnection.createOffer();
myPeerConnection.setLocalDescription(offer);
socket.emit('offer', offer, 1);
});
// Peer B가 합류를 받고 ok함(answer)
socket.on('offer', async offer => {
myPeerConnection.setRemoteDescription(offer);
const answer = await myPeerConnection.createAnswer();
myPeerConnection.setLocalDescription(answer);
socket.emit('answer', answer, 1);
});
// Peer A가 ok받는다.
socket.on('answer', answer => {
myPeerConnection.setRemoteDescription(answer);
});
이 연결 과정이 이해가 안 되시면 댓글로 남겨주시면, 답해드리겠습니다.
backend
/** 사람 수 */
let human = 0;
io.on('connect', socket => {
socket.on('client', data => {
console.log(data);
});
socket.emit('server', '서버입니다');
// 두명이상이 되면 클라이언트에 알린다.
socket.on('join', channel => {
socket.join(channel);
human += 1;
if (human > 1) {
socket.to(channel).emit('comeHere');
}
});
// offer 오면 전달함
socket.on('offer', (offer, channel) => {
socket.to(channel).emit('offer', offer);
});
// answer 오면 전달함
socket.on('answer', (answer, channel) => {
socket.to(channel).emit('answer', answer);
});
});
server 코드는 io.on 내부에서만 수정이 일어납니다. 내부 로직만 바꿔주시면 되고, 위에 human 변수를 추가해 줍니다.
이는 channel에 2명 이상이 되면 알리기 위한 변수입니다.
위의 로직을 보면 알 수 있듯이 user 끼리 소통을 하기 위해 서버가 중간점 역할을 하는 것이다.
step 5. iceCandidate
Connection을 위한 세팅은 했지만, 정작 중요한 브라우저의 video 정보를 보내지 않았습니다.
video 정보롤 주고받을 socket 통로를 열어주고, step 3에서 console.log만 찍고 나뒀던 로직을 마저 완성합니다.
backend
// video 정보를 보냄
socket.on('ice', (ice, channel) => {
socket.to(channel).emit('ice', ice);
});
우선 프론트는 로직 순서를 한번 정리하겠습니다.
socket.on 부분을 최상위로 끌어올리고, 'ice'로 받을 통로도 만들어 주었습니다. ( '추가한 로직 주석으로 표시했습니다.)
받을 준비가 안됐는데, emit을 호출하면 에러가 생길 수도 있으니 수정하였습니다.
import { io } from 'socket.io-client';
/** 본인 비디오 */
const myFace = document.querySelector('.myFace');
/** 카메라 on/off 버튼 */
const cameraBtn = document.querySelector('.camera');
/** 스피커 on/off 버튼 */
const speakerBtn = document.querySelector('.speaker');
/** 상대방 비디오 */
const peerFace = document.querySelector('.peerFace');
/** 본인 MediaStream 저장 */
let myStream;
/** speaker */
let muted = false;
/** video */
let cameraOff = false;
/** websocket으로 통신 */
const socket = io('ws://localhost:3000', {
withCredentials: true,
});
socket.on('connect', () => {
console.log('connect');
});
socket.on('server', data => {
console.log(data);
});
// 서버에서 사람이 들어오면 Peer A에 알립니다.
// 이로 인해 Peer A가 합류를 요청(offer) 합니다.
socket.on('comeHere', async () => {
const offer = await myPeerConnection.createOffer();
myPeerConnection.setLocalDescription(offer);
socket.emit('offer', offer, 1);
});
// Peer B가 합류를 받고 ok함(answer)
socket.on('offer', async offer => {
myPeerConnection.setRemoteDescription(offer);
const answer = await myPeerConnection.createAnswer();
myPeerConnection.setLocalDescription(answer);
socket.emit('answer', answer, 1);
});
// Peer A가 ok받는다.
socket.on('answer', answer => {
myPeerConnection.setRemoteDescription(answer);
});
socket.on('ice', ice => { // <= 추가한 로직
myPeerConnection.addIceCandidate(ice);
});
socket.emit('client', '클라이언트 입니다.');
/** 본인 비디오 연결 */
async function getMedia() {
try {
myStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
});
myFace.srcObject = myStream;
} catch (err) {
console.error('getMedia 과정에서 에러가 났습니다. \n', err);
}
}
/** Peer A 초기화 */
const myPeerConnection = new RTCPeerConnection();
function settingPeerConnection() {
myPeerConnection.addEventListener('addstream', data => {
console.log('addstream', data);
});
myPeerConnection.addEventListener('icecandidate', data => {
console.log('icecandidate', data);
});
myStream
.getTracks()
.forEach(track => myPeerConnection.addTrack(track, myStream));
}
// 비동기 제어
async function getMediaAndConnection() {
await getMedia();
settingPeerConnection();
}
getMediaAndConnection();
// 웹브라우저에 먼저 들어온 사람이 Peer A가 됩니다. 채널은 임의로 1번으로 고정하겠습니다.
socket.emit('join', 1);
RTCPeerConnection (연결 준비) => 본인 STREAM 정보 다듬고 RTCPeerConnection에 remote => 상대방한테 전송
=> 전송하는 사람은 본인의 stream정보를 myPeerConnection.createOffer()로 꺼내와서 보내고,
myPeerConnection에 setLocalDescription으로 추가,
받은 사람은 STREAM정보를 본인의 RTCPeerConnection에 setRemoteDescription()으로 포함
step 6. 마지막 퍼즐 console.log 부분 로직 채워 넣기
해당되는 부분에 2줄만 추가하면 됩니다. console.log를 찾아서 아래에 추가하시면 됩니다.
front
myPeerConnection.addEventListener('addstream', data => {
console.log('addstream', data);
peerFace.srcObject = data.stream;
});
myPeerConnection.addEventListener('icecandidate', data => {
console.log('icecandidate', data);
socket.emit('ice', data.candidate, 1);
});
data.candidate, data.stream 가 궁금하다면 콘솔 내용도 확인하고 공식문서도 확인하십시오. ( 살짝 지침.,.)
만약 여기까지 잘 따라오셨으면 localhost:4000으로 브라우저 2개를 열면 audio 증폭으로 인한 굉음이 들릴 것입니다.
잠깐 확인하고, 동작이 잘되면 다음 챕터로 넘어가시면 됩니다.
step. 7 video, audio on, off 기능
아래에 그냥 추가해주면 됩니다.
front
// speaker handler
function speakerOnOff() {
myStream.getAudioTracks().forEach(track => (track.enabled = !track.enabled));
if (!muted) {
speakerBtn.innerText = '스피커 on';
muted = true;
} else {
speakerBtn.innerText = '스피커 off';
muted = false;
}
}
// camera handler
function cameraOnOff() {
myStream.getVideoTracks().forEach(track => (track.enabled = !track.enabled));
if (!cameraOff) {
cameraBtn.innerText = '카메라 on';
cameraOff = true;
} else {
cameraBtn.innerText = '카메라 off';
cameraOff = false;
}
}
speakerBtn.addEventListener('click', speakerOnOff);
cameraBtn.addEventListener('click', cameraOnOff);
여기서 저희는 'RTCPeerConnection이 두 명의 user를 웹브라우저에서 상시 연결 시켜주는구나'를 예상할 수 있습니다.
실제로 공식문서 첫 설명을 보면,
chapter 4. 추가 사항
그러면 3개의 비디오 연결도 될까?
넵 가능합니다. 해당 방식은 p2p 통신 방식입니다.
user 3명이 접근하면 1명 개개인마다 나머지 2명한테 RTCPeerConnection을 통해 stream 정보를 주고받으면 됩니다.
위의 로직은 1대 1로 그리고, channel도 임의로 1을 넣으면서 동작 원리만 기록하였습니다.
웹브라우저를 3개를 열면 아래와 같은 에러를 보게 될 것입니다.

위의 에러를 해결하고 한번 확장성 있게 구현해보면 4명 5명도 될 것입니다.
한번 시도해 보시면, 좋은 공부가 될 것입니다.

마지막으로 완성본, 초기세팅본 repository 남기고 마치겠습니다.
https://github.com/kimjuno97/Implementing-Video-Chat-Using-the-WebRTC-API
해당 포스터는 후속편이 있습니다. 더 도움이 되길 바라는 마음으로 남깁니다.
감사합니다.
'기능 모음(JavaScript)' 카테고리의 다른 글
WebRTC로 서버 안태우고 라이브채팅 구현하기 (2) | 2023.01.08 |
---|---|
socket.io로 라이브 채팅 로직 구현하기 (0) | 2023.01.04 |
댓글