항해

WebRTC 코드로 적용해보기

ho-bolt 2022. 6. 9. 18:12

지난 번엔 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의 answersetRemoteDescription()로 세팅 해준다.

여기까지 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() 을 할 때 가져온 스트림을 꼭 가지고 와야만 그려줄 수 있다!!

728x90