WebRTC 코드로 적용해보기
지난 번엔 WebRTC에 대해 공부하여 정리했었다.
WebRTC 정리한거 보기
이제는 프로젝트에 적용하기 전 타 네트워크 간 연결을 해보기 위해 코드를 작성해보았다.
상황 피어는 총 2명이라 가정한다.
A가 먼저 방에 들어가고 그 다음 B가 들어간다.
A가 룸에 들어갔을 때 벌어지는 일
async function handleWelcomeSubmit(event) {
event.preventDefault();
const input = welcomeForm.querySelector('input');
//백엔드로 join_room 이벤트로 보내면 같은 이름의 이벤트로 받는다.
//방에 들어가기 전에 내 장치 가져옴
await initMedia();
console.log('방에 들어간다');
socket.emit('join_room', input.value);
roomName = input.value;
input.value = '';
}
A가 방에 이장을 누른 순간 위의 함수가 실행된다.
방에 들어가기 전에 먼저 initMedia() 함수를 통해 유저의 비디오, 오디오 정보를 가지고 온다.
initMedia()
async function initMedia() {
welcome.hidden = true;
call.hidden = false;
// 내 장치(카메라 오디오)를 가져옴
await getMedia();
makeConnection(socket.id);
}
getMedia()
async function getMedia(deviceId) {
const initialConstraints = {
audio: true,
video: { facingMode: 'user' },
};
const camerConstraints = {
audio: true,
video: { deviceId: { exact: deviceId } },
};
deviceId ? camerConstraints : initialConstraints
try {
myStream = await navigator.mediaDevices.getUserMedia(initialConstraints)
// myFace.volume = 0
paintMyFace(myStream);
// myFace.srcObject = myStream;
if (!deviceId) {
await getCamers();
}
} catch (err) {
console.log(err);
}
}
navigator.mediaDevices.getUserMedia(initialConstraints)
이 API를 통해 유저의 카메라와 마이크 정보를 가지고 온다.
그 다음 A의 피어, 네트워크 정보를 가져오기 위해 makeConnection()
함수를 실행한다.
makeConnection() 가장 중요한 함수
function makeConnection(remoteSocketId, remoteNickname) {
myPeerConnection = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stunserver.example.org" },
{
urls: "turn:52.79.93.143",
username: "booking",
credential: "booking1234",
},
],
});
//2명 이상일 때만 실행 됨.
myPeerConnection.addEventListener("icecandidate", (event) => {
handleIce(event, remoteSocketId);
});
myPeerConnection.addEventListener("track", (data) => {
handleAddStream(data, remoteSocketId, remoteNickname);
});
myStream
.getTracks()
.forEach((track) => myPeerConnection.addTrack(track, myStream));
if (screenStream) {
screenStream
.getTracks()
.forEach((track) => myPeerConnection.addTrack(track, screenStream));
}
// pcObj에 각 사용자와의 connection 정보를 저장함
pcObj[remoteSocketId] = myPeerConnection;
peopleInRoom++;
changeNumberOfUsers(`${peopleInRoom} / 10`);
return myPeerConnection;
}
function handleAddStream(data, remoteSocketId, remoteNickname) {
const peerStream = data.streams[0];
if (data.track.kind === "video") {
paintPeerFace(peerStream, remoteSocketId, remoteNickname);
}
}
async function paintPeerFace(peerStream, id, remoteNickname) {
try {
const videoGrid = document.querySelector("#video-grid");
const video = document.createElement("video");
const nickNameContainer = document.createElement("div");
const peername = document.createElement("div");
const div = document.createElement("div");
div.id = id;
video.autoplay = true;
video.playsInline = true;
video.srcObject = peerStream;
peername.innerText = `${remoteNickname}`;
peername.style.color = "white";
nickNameContainer.appendChild(peername);
div.appendChild(nickNameContainer);
div.appendChild(video);
video.className = "memberVideo";
peername.className = "nickName";
nickNameContainer.className = "nickNameContainer";
div.className = "videoBox";
videoGrid.appendChild(div);
// 입장시 현재인원들의 카메라 및 음소거 상태 확인
if (!checkEnterStatus.current[id]) {
return;
}
if (checkEnterStatus.current[id].screensaver) {
const screensaver = document.createElement("div");
screensaver.className = "screensaver";
div.appendChild(screensaver);
}
if (checkEnterStatus.current[id].muted) {
const muteIcon = document.createElement("div");
muteIcon.className = "muteIcon";
nickNameContainer.prepend(muteIcon);
}
} catch (error) {
console.log(error);
}
}
A는 STURN/TURN서버를 거쳐 피어의 공인IP주소를 가지고 온다 그리고 그 피어의 정보는 myPeerConnection 객체에 담는다.
그리고 가장 먼저 네트워크 정보를 가져오기 위해 ice framework를 이용해 네트워크 인터페이스와 포트를 찾는다.
handleIce
function handleIce(data, remoteSocketId) {
socket.emit('ice', data.candidate, remoteSocketId);
}
icecnadidate이벤트를 통해 가져온 event 데이터에서 candidate만 가져오고 시그널링 서버로 보내준다.
시그널링 서버 ( 백 )
socket.on('ice', (ice, remoteSocketId) => {
socket.to(remoteSocketId).emit('ice', ice, socket.id);
});
백에서 받아서 다시 ice를 보내준다.
프론트
socket.on('ice', async (ice, remoteSocketId) => {
await pcObj\[remoteSocketId\].addIceCandidate(ice)
});
A의 peer description 을 위한 candidate
를 추가하기 위해 addIceCandidate()
를 호출한다.
피어 (A) 얼굴 그려주기
function handleAddStream(data, remoteSocketId) {
const peerStream = data.streams[0]
if (data.track.kind === 'video') {
paintPeerFace(peerStream, remoteSocketId)
if (screenStream) {
screenShare.srcObject = screenStream;
}
}
}
그러면 이제 들어온 피어의 네트워크 정보, ip 주소까지 알게 되어 누군지 알게 된 상태이다.
그러면 스트림에서 비디오를 가지고 와 이제 화면에 그려준다.
여기가지가 A가 방에 들어갔을 때 벌어지는 일이다. 이제 B라는 피어가 들어갔을 때를 살펴보자
먼저 A가 들어갔을 때와 같이 B도 위와 똑같은 과정을 거친다. 하지만 한 가지 달라지는 게 있는데 바로 교환이 발생한다는 것이다!
백 시그널링 서버
socket.on('join_room', (roomName) => {
... 룸에 들어가는 로직 ~
socket.to(roomName).emit('joinStudyRoom', targetRoomObj.users, socket.id);
새로운 유저가 들어오면 welcome 이벤트를 발생 시킨다.
targetRoomObj.users는 해당 룸에 참여한 유저들의 소켓 ID가 담긴 배열이다.
welcome event ( 프론트 )
socket.on(
"joinStudyRoom",
async (userObjArr, socketIdformserver, videoType) => {
const length = userObjArr.length;
//카메라, 마이크 가져오기
await getMedia();
setSocketID(socketIdformserver);
changeNumberOfUsers(`${peopleInRoom} / 10`);
if (length === 1) {
return;
}
for (let i = 0; i < length - 1; i++) {
//가장 최근 들어온 브라우저 제외
try {
const newPC = makeConnection(
//RTCPeerconnection 생성
userObjArr\[i\].socketId,
userObjArr\[i\].nickname
);
const offer = await newPC.createOffer(); // 각 연결들에 대해 offer를 생성
await newPC.setLocalDescription(offer);
socket.emit("offer", offer, userObjArr\[i\].socketId, nickname); // offer를 보내는 사람의 socket id와 닉네임
} catch (error) {
console.log(error);
}
}
}
);
});
userObjArr에는 [A ,B]의 소켓 아이디가 이렇게 담겨 있을 것이다.
그러면 해당 유저의 참가자 수만큼 돌면서 교환이 발생하게 해준다.
A의 offer를 만들고 setLocalDescription()
을 통해 offer를 세팅해준다. 그리고 시그널링 서버로 offer를 보내준다.
시그널링 서버 (offer ) 부분
socket.on('offer', (offer, remoteSocketId, localNickname) => {
socket.to(remoteSocketId).emit('offer', offer, socket.id, localNickname)
})
A의 offer를 B에게 전달해준다.
프론트 (offer ) B
socket.on("offer", async (offer, remoteSocketId, remoteNickname) => {
try {
const newPC = makeConnection(remoteSocketId, remoteNickname);
await newPC.setRemoteDescription(offer);
const answer = await newPC.createAnswer();
await newPC.setLocalDescription(answer);
socket.emit("answer", answer, remoteSocketId);
} catch (error) {
console.log(error);
}
});
B는 A의 offer
를 받고 setRemoteDescription()
를 통해 offer를 세팅해주고 answer
를 만들어준다.
그리고 answer
를 다시 시그널링 서버로 보내준다.
시그널링 서버 (answer) 부분
socket.on('answer', (answer, remoteSocketId) => {
// 받은 answer
socket.to(remoteSocketId).emit('answer', answer, socket.id)
})
받은 B의 answer를 A에게 전달해준다.
프론트 (answer ) A
socket.on("answer", async (answer, remoteSocketId) => {
await pcObj\[remoteSocketId\].setRemoteDescription(answer);
});
B의 answer
를 setRemoteDescription()
로 세팅 해준다.
여기까지 A와 B의 offer와 answer가 교환이 끝났다.
프론트 (ice)
socket.on("ice", async (ice, remoteSocketId) => {
await pcObj\[remoteSocketId\].addIceCandidate(ice);
});
그러면 이제 세팅한 description
을 위해 addIceCandidate
를 해준다.
여기서 상대방의 얼굴을 보이 위해서 중요한 부분이 바로 makeConnection 함수의 handleAddStream()
이다
handleAddStream()
function handleAddStream(data, remoteSocketId, remoteNickname) {
const peerStream = data.streams\[0\];
if (data.track.kind === "video") {
paintPeerFace(peerStream, remoteSocketId, remoteNickname);
}
}
paintPeerFace는 가져온 비디오 스트림을 html에 그려주는 함수이다.makeConnection()
을 할 때 가져온 스트림을 꼭 가지고 와야만 그려줄 수 있다!!