Etc

[WebRTC] 웹 RTC 구현해보기 - N : N 연결

천우산__ ㅣ 2023. 8. 7. 17:24

목적

web Rtc 를 이용하여 클라이언트 간 1 : 1 영상 및 오디오 연결을 진행한다.

이용한 기능들

Front : html , javascript, socket.io

back : Nest js (gateway)

기본 세팅

로컬 비디오 연결 설정 및 피어 정보 연결 설정은 기존과 같음

차이점 

이전 글의 경우, 비디오 / 오디오 연결을 위한 코드 흐름이 1 :1에만 초점이 되어 있어, 두 번째 이후 사용자에 연결에 대한 

대응이 불가능하다. 다중 사용자 연결을 위해서는 연결 정보를 보관하는 변수 ( 이전 글의 경우, peerInfo ) 를 Object 형으로

관리하여 여러 사용자 간의 연결 정보를 보관해야 한다

Object 형을 활용하여 다중 사용자간 연결 구현 예시

다음으로, 신규 사용자와의 연결을 진행할 때, 이미 연결된 사용자와의 재연결은 막아야한다.

그렇지 않으면, 이미 연결된 두 클라이언트 간의 연결 상태가 stable 한데, 다시 연결하는 것에 대해서 경고가 발생하기 때문이다. 

연결 정보를 소켓으로 주고 받는 특성 상, 특정 room에 있는 클라이언트를 대상으로 메세지를  보낼 수 있지만

특정 사용자를 대상으로 메세지를 보낼 수는 없기 때문에, 신규 클라이언트 ( C )가 진입하고, 연결 시도가 발생하면

이미 연결된 클라이언트 들( A, B )도 연결 정보를 교환한다.

1:1 연결 이후 새로운 사용자가 들어왔을 때의 시나리오

기존 클라이언트와 새로 연결을 시도하는 과정을 막을 수는 없지만, 연결을 시도하는 과정에서 이미 연결된 클라이언트임을 식별하고

연결 정보가 확인되는 클라이언트는 연결 정보를 교환하지 않도록 코드로 조치한다.

정리

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' 카테고리의 다른 글

[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
REST? RESTful?  (0) 2023.04.20