목적
web Rtc 를 이용하여 클라이언트 간 1 : 1 영상 및 오디오 연결을 진행한다.
이용한 기능들
Front : html , javascript, socket.io
back : Nest js (gateway)
기본 세팅
로컬 비디오 연결 설정 및 피어 정보 연결 설정은 기존과 같음
차이점
이전 글의 경우, 비디오 / 오디오 연결을 위한 코드 흐름이 1 :1에만 초점이 되어 있어, 두 번째 이후 사용자에 연결에 대한
대응이 불가능하다. 다중 사용자 연결을 위해서는 연결 정보를 보관하는 변수 ( 이전 글의 경우, peerInfo ) 를 Object 형으로
관리하여 여러 사용자 간의 연결 정보를 보관해야 한다
다음으로, 신규 사용자와의 연결을 진행할 때, 이미 연결된 사용자와의 재연결은 막아야한다.
그렇지 않으면, 이미 연결된 두 클라이언트 간의 연결 상태가 stable 한데, 다시 연결하는 것에 대해서 경고가 발생하기 때문이다.
연결 정보를 소켓으로 주고 받는 특성 상, 특정 room에 있는 클라이언트를 대상으로 메세지를 보낼 수 있지만
특정 사용자를 대상으로 메세지를 보낼 수는 없기 때문에, 신규 클라이언트 ( C )가 진입하고, 연결 시도가 발생하면
이미 연결된 클라이언트 들( A, B )도 연결 정보를 교환한다.
기존 클라이언트와 새로 연결을 시도하는 과정을 막을 수는 없지만, 연결을 시도하는 과정에서 이미 연결된 클라이언트임을 식별하고
연결 정보가 확인되는 클라이언트는 연결 정보를 교환하지 않도록 코드로 조치한다.
정리
N : N 으로 연결하기 위해서 1 : 1 연결 구현과는 조금 다르게 코드를 구성해야 한다.
1. 클라이언트 간 연결 정보를 Object 로 관리한다. ( Front )
2. 클라이언트 식별을 위해서 연결 정보 ( 메세지 ) 송신 주체가 누구인지 알 수 있도록 구현해야 한다 ( Back )
3. 각 클라이언트는 연결 정보를 교환할 때, 이미 연결된 클라이언트인지 확인 후, 연결이 되지 않은 클라이언트만 연결 정보를 교환한다.
메세지 송신 주체를 확인하는 것은 다양한 방법으로 구현할 수 있겠지만, 여기서는 socket에서 제공하는 client id로 구현하였다.
// 백엔드 코드
export class webRtcGateway{
@WebSocketServer() server: Server;
// 1단계 - 입장 알림
@SubscribeMessage("join")
async handleJoinMessage(client: Socket, roomId: any){
client.join(roomId);
client.broadcast.to(roomId).emit("enter", { userId: client.id });
}
// 2단계 - 연결 요청
@SubscribeMessage("offer")
handleOfferMessage(client: Socket, { offer, selectedRoom }){
client.broadcast.to(selectedRoom).emit("offer", { userId: client.id, offer });
}
// 3단계 - 응답 생성
@SubscribeMessage("answer")
handleAnswerMessage(client: Socket, {answer, toUserId, selectedRoom}){
client.broadcast.to(selectedRoom).emit("answer", {
userId: client.id,
answer,
toUserId,
});
}
// 4단계 - 연결 후보 교환
@SubscribeMessage("icecandidate")
handleIcecandidateMessage(client: Socket, { candidate, selectedRoom }){
client.broadcast.to(selectedRoom).emit("icecandidate", { userId: client.id, candidate });
}
}
// 클라이언트 (Front) 코드
let localVideo = document.getElementById("localVideo");
let remoteVideo = document.getElementById("remoteVideo");
let localStream;
let peerConnection;
let peerInfo = {};
let selectedCandidate = {};
const makePeerConnect = async(userId) => {
peerInfo[userId] = new Object();
peerInfo[userId].peerConnection = new RTCPeerConnection({
"iceServers": [{
urls: 'stun:stun.l.google.com:19302'
}]
});
peerInfo[userId].peerConnection.addEventListener("icecandidate", icecandidate);
peerInfo[userId].peerConnection.addEventListener("addstream", addStream);
for (let track of localStream.getTracks()) {
await peerInfo[userId].peerConnection.addTrack(track, localStream);
}
};
// socket
let socket = io("http://localhost:3000");
socket.on('enter', async({
userId
}) => {
await makePeerConnect(userId);
const offer = await peerInfo[userId].peerConnection.createOffer();
await peerInfo[userId].peerConnection.setLocalDescription(offer);
socket.emit("offer", offer);
});
socket.on("offer", async({
userId,
offer
}) => {
if (!peerInfo[userId]) {
await makePeerConnect(userId);
await peerInfo[userId].peerConnection.setRemoteDescription(offer);
const answer = await peerInfo[userId].peerConnection.createAnswer(offer);
await peerInfo[userId].peerConnection.setLocalDescription(answer);
socket.emit("answer", {
answer,
offer,
toUserId: userId
});
}
});
socket.on("answer", async({
userId,
answer,
responseOffer,
toUserId
}) => {
if (peerInfo[toUserId] === undefined) {
await peerInfo[userId].peerConnection.setRemoteDescription(answer);
};
})
socket.on("icecandidate", async({
userId,
candidate
}) => {
if (selectedCandidate[candidate.candidate] === undefined) {
selectedCandidate[candidate.candidate] = true;
await peerInfo[userId].peerConnection.addIceCandidate(candidate);
};
})
socket.on("userDisconnect", ({
userId
}) => {
delete peerInfo[userId];
//const disconnectUser = document.getElementById(userId);
//disconnectUser.remove();
})
const useMedia = async() => {
await getMedia();
}
const share = async() => {
socket.emit('join', '1234');
}
// 내 비디오 & 오디오 정보를 가져옵니다.
const getMedia = async() => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
localStream = stream;
localVideo.srcObject = stream;
} catch (error) {
console.log(error);
}
};
// 연결 후보 교환
const icecandidate = (data) => {
if (data.candidate) {
socket.emit("icecandidate", data.candidate);
}
}
// 상대 영상 & 비디오 추가
const addStream = (data) => {
let videoArea = document.createElement("video");
videoArea.autoplay = true;
videoArea.srcObject = data.stream;
let container = document.getElementById("container");
container.appendChild(videoArea);
};
useMedia();
'Etc' 카테고리의 다른 글
[Vscode] 저장 시 Prettier 적용하기 (1) | 2024.11.19 |
---|---|
[WebRTC] 웹 RTC 구현해보기 - 1 : 1 연결 (3) | 2023.07.15 |
[ETC] vscode 환경에서 ssh 이용하기 (0) | 2023.05.20 |
[DataBase] SQL ? NoSQL? 그리고 ORM (2) | 2023.05.11 |
[Nodejs] package.json 이란? (0) | 2023.05.02 |