이제서야 쓰는 WebRTC
를 활용한 화상 비디오 회의 구현부 포스팅이다.
사실 1편 이론부를 작성하고 구현부도 후다닥 작성하고 싶었으나,, 미루고 미루다 한달이 넘어버렸다. 허허...
해당 포스팅에서 이론 포스팅에서 다뤄봤던 내용을 기반으로 실제로 화상 회의 기능 구현을 해보도록 하겠다. 주된 기술 스택을 정리해보자면 다음과 같다.
Front-End :
Vue.js
,WebRTC
Simple-Peer Library
,Stomp
Back-End :Spring boot
,WebRTC
,Stomp
WebRTC
를 구현하는데 보통의 구글링 자료들에서 SocketIO
와 Node.js
를 많이 사용하는 것도 따로 이유가 있었다. WebRTC
는 각각의 클라이언트 사이의 소켓을 통해서 수많은 데이터 통신이 일어나는데, 이때 무거운 Spring boot
보다는 가벼운 Node.js
를 쓰는게 낫다는 것이다.
하지만 나는 취업을 위해 K-Backend인 Spring boot
를 공부하는 입장이니 아묻따 Spring boot
로 개발을 시작했다.
구현함에 있어서는 나는 Mesh 방식
을 선택하여 전형적인 P2P 통신을 구현하고자 했고(Mesh 방식
에 대한 소개는 1편 이론 포스팅 참고), 그렇기 때문에 서버 사이드에서는 단순히 소켓 통신 서버를 열어두고 각각 클라이언트 Peer Data를 중개해주기만 하면 됐다(Sinaling Server). 때문에 실제로 서버 사이드의 코드 양은 매우 적다!
Spring boot
를 사용했기 때문에 이러한 소켓 통신은 STOMP
를 사용했다.
그리고 WebRTC
는 그대로 사용하기에는 너무 어렵다ㅠㅡㅠ 물론 WebRTC
의 Deep한 부분까지 개발을 할 때 조정해야 했다면, WebRTC
를 raw하게 사용했겠지만,, 이번 프로젝트는 화상 회의 구현이 목적이었기 때문에 WebRTC
사용을 도와주는 라이브러리를 사용하기로 했다. 이러한 라이브러리는 매우 많았는데 그 중 Simple-Peer
라이브러리를 사용했다. Simple-Peer 라이브러리 깃헙에 예시 코드도 많고 활용법이 가이드가 깔끔하게 작성돼 있어서 참고할만 하다!
또한 정말 많은 도움을 받은 Velog 글과 유튜브 영상이 있다. 해당 Velog 글에서 쏘스 참고를 정말 많이 했고, 유튜브는 WebRTC
관해서 여러편 강의 영상을 올라와 있는데 참고할 만한 부분이 정말 많다ㅠㅡㅠ. Simple-Peer
라이브러리를 채택한 이유도 해당 유튜브 영상에서 사용했기 때문이다. 나도 해당 유튜브 영상의 코드를 많이 공부해보고 적용시켜 보았다.
Mesh 방식
을 활용한다면 1대 1 화상회의를 구현하는 것은 어렵지 않다. 하지만 다대 다 화상회의를 구현하는 것은 또 다른 문제이다. 오늘 포스팅에서 그 부분까지 내가 구현한 코드를 훑어보도록 하겠다.
다시금 WebRTC
의 과정을 상기해보는 것이 중요하다. Mesh
방식으로 구현된 WebRTC
는 Signaling Server
를 통해 클라이언트 Peer 데이터를 전달하고 전달 받으면서 Peer간 연결된다. 아래 동작 과정을 한번더 살펴보면 이해하기 훨씬 쉬울 것이다.
① Client Side의 A Client(Peer)에서Signaling Server
로 연결에 필요한 A Clinet의 데이터를 보낸다.
=>Signaling Offer
② Server Side에서,Signaling Server
에 연결된 모든 세션들에게 A Client의 데이터를 전달한다.
③ Client Side의 B Client(Peer)에서 A Client의 데이터를 활용해서 연결에 필요한 일련의 작업을 한 후, B Client의 데이터를Signaling Server
로 보낸다.
=>Signaling Answer
④ Server Side에서, A Client의 세션에게 B Client의 데이터를 전달한다.
⑤ 각각의 데이터를 활용하여 WebRTC가 A Client와 B Client가 연결한다.
아래로는 Simple-Peer 라이브러리를 통해서 다대다 연결을 구현한 코드이다.
Simple-Peer 라이브러리 깃헙의 2명 이상의 P2P 연결 부분을 참고하면 좋다.
또한 아래 코드들은 이해를 돕기 위한 예시 코드이다!! 전체 코드는 깃헙(FE), 깃헙(BE)에서 확인 가능하나 다른 기능과 처리 코드도 섞여 있기 때문에 참고하기 어려울 수 있다.
해당 코드에서는 예시로 3명의 Client가 연결한다고 가정해보겠다.
아래 코드 플로우 시나리오대로 코드를 구현한다고 보면 된다!!
코드 플로우 시나리오
1. A Client 세션 접속.
2. B Client 세션 접속.
3. B Client 세션이 새로 접속했다는 사실을 A Client에게 소켓을 통해 전달.
4. A Client가 B Client에게 Signaling Offer.
5. B Client가 A Client에게 Signaling Answer.
6. A Client와 B Client 사이에 P2P 연결 성공.
- C Client 새로 세션 접속.
- C Client가 새로 접속했다는 사실을 A, B Client에게 전달.
- A, B Client가 C Client에게 Signaling Offer.
- C Client가 A,B Clinet에게 Signaling Answer.
- A, B, C Client와 P2P 연결 성공.
<!--Vue.js Html-->
<!--예시로 3개의 video 태그-->
<video ref="aaa_video" autoplay/>
<video ref="bbb_video" autoplay/>
<video ref="ccc_video3" autoplay/>
// Vue.js Js
import Peer from "simple-peer";
import SockJS from "sockjs-client";
import Stomp from "webstomp-client";
let socket;
let stomp;
data() {
// 접속한 id
myId: "",
// caller의 stream 저장
callerStream: "",
// 자신과 연결된 세션 peer를 저장
peers: []
}
},
//... 중략
methods: {
// 자신의 video 태그 연결 & stream 저장하는 메소드
// mounted() 에서 사용
async userSet() {
await navigator.mediaDevices
.getUserMedia({
video: true,
audio: true,
})
.then((stream) => {
// stream 추출
let videoStream = new MediaStream(stream.getVideoTracks());
this.callerStream = stream;
// video 태그와 연결
this.$refs[this.myId + "_video"].srcObject = videoStream;
// 소켓 통신 연결 메소드
// 아래 기술.
this.connect();
});
},
// ... 중략
connect() {
// Stomp 소켓 통신 선언부
socket = new SockJS(Constants.API_URL + "/socket");
stomp = Stomp.over(socket);
// subscribe&pub 정의
stomp.connect(
{},
// connectCallback
() => {
// 누군가 join 했을때 listen, 접속해 있는 전체 세션 리스트를 받는다.
stomp.subscribe("/sub/video/joined-room-info", (data) => {
// 접속해 있는 전체 세션 리스트
let users = JSON.parse(data.body);
// 마지막으로 접속한 user
let topIdx = users.length - 1;
let joinedID = users[topIdx].id;
// 인원이 한명 이하거나, 자신이 join 일경우는 return
if (topIdx <= 0 || users[topIdx].id === this.myId) return;
// 아래 기술
// 자신이 접속해 있는 상태에서, 새로운 클라이언트가 접속한 경우,
// 해당 클라이언트와 연결하기 위한 메소드
this.initCall(joinedID);
});
// 자신이 접속했다는 socket send
stomp.send(
"/pub/video/joined-room-info",
JSON.stringify({from: this.myId})
);
// ... 중략
},
// onErrorCallback
() => {
console.log("ws error");
});
},
// ... 중략
},
// Spring boot
public class VideoRoomController {
// 테스트용 세션 리스트.
private final ArrayList<TestSession> sessionIdList;
private final SimpMessagingTemplate template;
// 실시간으로 들어온 세션 감지하여 전체 세션 리스트 반환
@MessageMapping("/video/joined-room-info")
@SendTo("/sub/video/joined-room-info")
private ArrayList<TestSession> joinRoom(@Header("simpSessionId") String sessionId, JSONObject ob) {
// 현재 들어온 세션 저장.
sessionIdList.add(new TestSession((String) ob.get("from"), sessionId));
return sessionIdList;
}
// ... 중략
}
Caller
가 되고,Callee
가 된다.// Vue.js Js
// ... 중략(in method)
// 새로운 client가 접속했을 때, 해당 클라이언트와 연결할 Peer을 생성
initCall(joinedID) {
// peer 생성
// sinmple peer 라이브러리
const peer = new Peer({
initiator: true,
trickle: false,
stream: this.callerStream,
});
// caller의 signaling data를 새로 들어온 클라이언트에 send
peer.on("signal", (data) => {
// calling을 시작한 클라이언트(caller)의 singal 데이터 socket send
stomp.send(
"/pub/video/caller-info",
JSON.stringify({
toCall: joinedID,
from: this.myId,
signal: data,
})
);
});
// 새로 들어온 클라이언트 video 연결
peer.on("stream", (stream) => {
this.$refs[joinedID + "_video"].srcObject = stream;
});
peer.on("error", (stream) => {
console.log("error");
});
// peer 저장.
this.peers.push([peer, this.myId, joinedID]);
},
// ... 중략
// Spring boot
public class VideoRoomController {
// ... 중략
// caller의 데이터를 그대로 전달.
@MessageMapping("/video/caller-info")
@SendTo("/sub/video/caller-info")
private Map<String, Object> caller(JSONObject ob) {
return ob;
}
}
callee
는 caller
에게 온 Signaling data
를 이용하여 촤종 Signaling => Peer to Peer 연결
// Vue.js Js
// ... 중략(in method)
// connect 메소드 추가 구현
connect() {
// .. 중략
stomp.connect(
{},
() => {
//.. 중략
// caller의 info를 담은 socket lieten,
stomp.subscribe("/sub/video/caller-info", (data) => {
data = JSON.parse(data.body);
// 나에게서 오거나(from me) 혹은 나에게 온(to me)이 아니면 return
if (data.from === this.myId || data.toCall !== this.myId) return;
// 아래 구현
// callig을 받은 시점에, return call을 보내 signaling한다.
// caller의 데이터를 받고, 내(callee)의 데이터를 보내는 과정
this.returnCall(data.signal, data.from);
});
});
},
// caller에게 요청을 받은 상태에서 callee의 signal data return
returnCall(callerSignal, callerId) {
// callee의 peer 생성
const peer = new Peer({
initiator: false,
trickle: false,
stream: this.callerStream,
});
// callee의 정보를 caller에게 보냄.
peer.on("signal", (data) => {
stomp.send(
"/pub/video/callee-info",
JSON.stringify({
from: this.myId,
to: callerId,
signal: data,
})
);
});
// caller 의 비디오 연결
peer.on("stream", (stream) => {
this.$refs[callerId + "_video"].srcObject = stream;
});
peer.on("error", (stream) => {
console.log("error");
});
// callee와 caller의 연결. => Signaling
// 이 시점에서 연결된다고 볼 수 있다.
peer.signal(callerSignal);
// 연결된 peer 리스트에 push
this.peers.push([peer, this.myId, callerId]);
},
// Spring boot
public class VideoRoomController {
// ... 중략
// callee의 데이터를 그대로 전달.
@MessageMapping("/video/callee-info")
@SendTo("/sub/video/callee-info")
private Map<String, Object> answerCall(JSONObject ob) {
return ob;
}
}
처음 구현할 때, 많이 돌아서 돌아서 코드를 치게 됐는데,, 막상 다 치고 나니까 정말 간단한 구조이다. Singnaling Server
는 단순히 클라이언트끼리의 Signal Data
를 중개해주는 역할만을 하며 직접적인 연결은 클라이언트 사이드에서 이루어진다.
그렇기 때문에 해당 방식(Mesh 구조의 방식)은 1대 1 연결에 적합한 구조이다. 실제로도 AWS 프리티어에 올려놓고 테스팅 해보니까 4명만 접속해도 매우 버벅이는 것을 확인할 수 있었다.. 해당 문제는 프리티어 클라우드 서버의 낮은 퍼포먼스 때문일수도 있고 내 프론트 단의 코드 구조가 문제일 수도 있다.
그럼에도 불구하고 Simple-Peer
라는 라이브러리를 활용해서 비교적 간단하게 구현할 수 있었던 것 같았고 서칭하다가 매우 많은 WebRTC
사용에 도움을 주는 라이브러리와 오픈 소스들을 발견할 수 있었다.
AWS Kinesis
, Open Vidu
, Jitsi
정도를 발견했는데, WebRTC
를 실제 프로덕트 수준으로 개발하려면 해당 기술을 찾아보는 것을 추천한다.
또한 위의 코드에서 STUN Server
와 TURN Server
에 대한 내용은 빠졌는데, Simle-Peer 라이브러리
에서는 해당 서버들이 구현만 돼있다면(구글 스턴 서버처럼 오픈된 서버를 사용한다면) 코드 한 줄로 적용할 수 있다. 해당 내용은 라리브러리 깃헙 레포를 확인해보면 예제가 있다.
아무쪼록 위의 코드는 전문은 깃헙(FE), 깃헙(BE) 에서 확인할 수 있긴한데,, 위의 코드에 비해 다른 기능들을 넣어놓은 것들이 많아서 참고하기 힘들 수도 있다.. 허허..
관련 프로젝트에 관심이 생겨서 WebRTC
에 대해 여러가지로 알아봤는데,, 상용화할 정도로 개발하는 것은 실제로 정말 어렵다고 한다.
나도 어느정도 예외 대충 잡고 "이정도면 대충 돌아가겠지~"하고 서버 올려서 돌려보니까 어마어마하게 예민한(?) 화상 회의 기능이 만들어졌다.. 예민한다는 것은 뭐만하면 팅기고 갑자기 안되고 난리가 나는..
아무쪼록 나는 처음 시작할 때 방향 잡기가 너무 힘들어서 해당 포스팅을 작성해야겠다고 맘 먹었는데,,, 나와 똑같은 상황에 놓여있는 분들에게 조금이라도 참고가 되면 좋겠다!
https://velog.io/@thms200/WebRTC-%ED%99%94%EC%83%81-%EC%97%B0%EA%B2%B0-%ED%95%98%EA%B8%B0
https://www.youtube.com/watch?v=R1sfHPwEH7A&list=PLK0STOMCFms4nXm1bRUdjhPg0coxI2U6h&index=
좋은 글 감사합니다. :)