# 3.3 -7 WebRTC

이원규·2022년 6월 18일

Zoom clone coding

목록 보기
16/18

1. WebRTC

  • Web Real Time Communication을 뜻함. 실시간 커뮤니케이션을 가능하게 해주는 기술. 실시간으로 비디오, 영상, 채팅까지 보낼 수 있음.
    WebRTC API

1.1 peer-to-peer

  • not peer-to-peer: 이전에 socketIO-websocket를 이용해서 우리가 msg를 서버에게 보내고 서버가 모두에게 그 msg를 전달해줬음. 이건 그 상대방에게 바로 간 것이 아니라 서버를 거쳐간 것임.
  • peer-to-peer: 즉, 우리의 메시지나 오디오, 영상 등이 서버로 가는 것이 아니라 상대방에게 바로 가는 것임. 이건 user끼리 소통할 때 서버가 필요없음(서버는 있어야함.). 그래서 실시간이 속도가 굉장히 빠른 이유기도함. WebRTC는 우리가 peer-to-peer연결이 가능하게 함. 그럼 더 이상 서버에 전달 X, 컴퓨터(브라우저)에 직접 전달하면 내 브라우저가 상대 브라우저에 연결되어 바로 전달할 것임.
  • signaling: 두 브라우저가 연결되기 전, 상대 브라우저가 서버의 어디에 있는지 정보가 없음. 이를 위해 signaling이 필요한데, 이는 각 상대의 브라우저가 서버 어디에 있는지 서버가 브라우저한테 알려주는 과정임. peer-to-peer를 하기 위해 서버에 signaling을 해줘야함. signaling이 끝나면 peer-to-peer이 연결됨.
    이 webRTC에서 서버의 역할은 그저, 브라우저에게 상대 브라우저가 어디에 있는지 정보(datas)를 제공하는 것임.

-> 앞으로의 과정: socketIO사용, signaling을 위해 webSocket을 사용하고, peer-to-peer가 되면 이전에 만든 stream을 보내줄 것임.

2. room만들기(videoCall)

2.1 room 만들기

(frontend)

  • home.pug
main
            div#welcome
                form
                    input(placeholder="room name", required, type="text")
                    button Enter room
            div#call
                div#myStream
                    video#myFace(autoplay,playsinline, width="400", height="400")
                    button#mute Mute
                    button#camera Turn Camera Off
                    select#cameras 
  • app.js
//추가 
const call = document.getElementById("call");
call.hidden = true;

let roomName;
//Welcome Form (join a room)
const welcome = document.getElementById("welcome");
const welcomeFoam = welcome.querySelector("form");

async function startMedia(){
    welcome.hidden = true;
    call.hidden = false;
    await getMedia();
}

function handleWelcomSubmit(event){
    event.preventDefault();
    const input = welcomeFoam.querySelector("input");
    frontSocket.emit("join_room", input.value, startMedia);
    input.value="";
}

welcomeFoam.addEventListener("submit", handleWelcomSubmit);

//삭제
getMedia();

backend

  • server.js
//추가
ioServer.on("connection",(backSocket) => {
    backSocket.on("join_room", (roomName, done) =>{
        backSocket.join(roomName);
        done();
    });
});

2.2 다른 유저가 방에 참가.

frontend

  • app.js
//추가

//Socket Code
frontSocket.on("welcome", () => {
    console.log("someone joined");
});

backend

  • server.js
//추가 
backSocket.to(roomName).emit("welcome");

3 방에 참가한 뒤, peer-to-peer연결되도록 하기 -> WebRTC사용


-> 위의 사진 순서를 따라갈 것임.
-> addStream을 하기 전에, 양쪽 브라우저에 RTC연결을 먼저 해야함. 우선 양쪽 브라우저의 연결을 만들어야함. 양쪽에서 연결통로를 만들어서 그것들을 같이 연결할 것임. 중요한 점은, 이 연결들이 일단, 따로 설정이 이루어질 것이고, 그것들을 (server)socketIO를 이용해서 이어줄 것임.
-> getUserMedia, addStream은 건너뜀. 왜냐하면 이미 손님 유저가 들어올 때 실행되기 때문임.

3.1 양쪽 브라우저에서 각각 연결통로 만들기.

frontend

-> signaling
1. 1단계 : 우리가 peer connection을 두 브라우저 사이에 만듦.
2. 2단계 : 우리가 하는 이 stream의 데이터를 가져다가 연결을 만들 것임.
3. 3단계 : createOffer만들기, offer: 우리의 정보를 알려주는 것(?) 약간 초대장 같은거임. -> cosole.log(offer)

4. 4단계 : 방금 만든 offer로 연결 구성하기(주인장 쪽 콘센트 꽂기 같은거), setLocalDescription이용. 이 offer는 이미 들어와있던 사람에게만 보이는 offer임.
5. 5단계 : offer를 backend로 보내기.
-----> backend 에서 답장 to(roomName).emit("offer", offer)함------
6. 6단계 : backend에서 보내준 offer를 받음. 이 offer는 방금 새로 들어와서 4단계에서 offer를 못받은 브라우저를 위한 것임.
우리는 비디오, 오디오 등을 주고 받을 땐, 서버가 필요없지만, offer를 주고 받을 땐, 서버가 필요함.
-> offer를 주고 받은 순간, 우리는 직접적으로 대화할 수 있음.
7. 7단계 : 들어온 사람 방금 받은 offer로 remote로 연결하기.(손님 쪽 remote 콘센트 꽂기 같은거). setRemoteDescription 사용함. -> 이 때 처리 속도가 너무 빨라서 stream데이터를 myPeerConnection에 넣기도 전에 offer를 제공함. 즉, 속도가 너무 빨라서 손님 유저가 offer를 받지 못함. 그래서 initCall()함수를 join_emit으로부터 밖으로 빼주고 await을 함.
8. 8단계 : 손님 유저의 offer에 대한 answer 생성하기.
9. 9단계 : 손님 유저의 setLocalDescription (local에다가 콘센트 꽂기)실행해주기. -> 7단계에서는 remote로 연결했고, 여기서는 local에 연결하는 거임.
10. 10단계 : 주인장 쪽 브라우저로 answer보내기 (잘 연결됐다고 !)
--> backend 에서 answer받고 다시 room으로 보냄 to(roomName).emit("answer",answer)--
11. 11단계 : 손님 유저가 보낸 answer를 주인장이 받아서 remote로 콘센트 연결함.
12. 12단계 : IceCandidate(인터넷 연결 생성) : 멀리 떨어진 두 브라우저가 서로 소통할 수 있도록 해주는 방법. 중재하는 프로새스 같은 것암. 어떤 소통 방법이 젤 좋을지를 제안할 때 쓰는거. 즉, IceCandidate(인터넷 연결 생성)는 브라우저를 소통하도록 하는 여러가지 방법을 가지고 있는데, 이 중 젤 적합한 방식으로 소통할 수 있도록 방법을 제공햐줌. -> IceCandidate를 생성하기
13. 13단계 : IceCandidate를 다른 브라우저로 보내기 위헤 서버로 보내기(두 브라우저 각각 서로의 candidate를 서로에게 보내줘야함.) -> 밑에 적어준 코드가 서로에게 실행되기 때문에 서로에게 보낸 것임 ㅇㅇ.
--> backend에서 candidate받아서 다시 방에 있는 브라우저한테 보내주기.-------
14. 14단계 : 보낸 candidate받기(두 브라우저에게 동시 적용됨.) 즉, 이 한 코드가 두 브라우저에게 적용되어 두 브ㄹ라우저가 동시에 candidate를 교환(?)할 수 있음 ㅇㅇ
15. 15단계 : addStream하기. 즉, 상대방의 stream을 추가한 뒤, media를 가져올 것임.(두 브라우저에게 서로 적용되는 코드임. 서로가 상대방의 stream 추가) -> 이렇게 해주면 상대방의 video를 볼 수 있음. 하지만 상대방의 카메라 장치가 바뀌었을 때 적용되지 않음. -> 다음 강의에서 이걸 포함한 몇가지 버그를 고칠것임

-> 결론적으로 두 브라우저 모두 local, remote Description을 갖도록 해줘야함. 또한 우리가 offer와 answer, IceCandidate를 양쪽에서 교환해야됨. 이후 상대방의 stream을 추가하면 비디오가 나옴.ㄴ

  • home.pug
//#peersStream video 생성 ㄱ ㄱ// (15단계: 상대 stream을 가져오고 media넣을 칸을 만듦.)
main
            div#welcome
                form
                    input(placeholder="room name", required, type="text")
                    button Enter room
            div#call
                div#myStream
                    video#myFace(autoplay,playsinline, width="400", height="400")
                    button#mute Mute
                    button#camera Turn Camera Off
                    select#cameras 
                    video#peerFace(autoplay,playsinline, width="400", height="400")
  • app.js
//추가 및 수정
let myPeerConnection;

async function startMedia(){
    welcome.hidden = true;
    call.hidden = false;
    await getMedia();
    makeConnection();// <- 여기가 추가됨.(이 함수가 실제로 연결을 만드는 함수가 될것임.
}

async function initCall(){
    welcome.hidden = true;
    call.hidden = false;
    await getMedia();
    makeConnection();
}

async function handleWelcomSubmit(event){
    event.preventDefault();
    const input = welcomeFoam.querySelector("input");
    await initCall();//-> 원래 join_room emit에 있었음. 근데 유저가 방에 들어오고 이 함수가 실행되면 처리 속도가 너무 빨라서 뒤에 들어온 손님 유저가 offer를 받지 못해서 이렇게 밖으로 빼줌.
    frontSocket.emit("join_room", input.value);
    roomName = input.value;
    input.value="";
}

//Socket code
frontSocket.on("welcome", async () => {
    const offer = await myPeerConnection.createOffer();//3단계 : offer(초대코드) 만들기 -> 이미 들어와있던 사람에게만 작용하는 코드임.
    myPeerConnection.setLocalDescription(offer);//4단계: offer로 연결 구성. -> 이미 들어와있던 사람에게만 작용하는 코드임.
    //console.log(offer);
    frontSocket.emit("offer",offer,roomName);//5단계: offer 보내기
});// -> 주인쪽 브라우저에서 돌아가는 코드

frontSocket.on("offer", async(offer) => {//6단계: offer받기
    myPeerConnection.setRemoteDescription(offer);//7단계: 손님쪽 콘센트 연결
    const answer = await myPeerConnection.createAnswer();// 8단계: answer 생성.
    //console.log(answer);
  	myPeerConnection.setLocalDescription(answer);//9단계 : 손님쪽 local 콘센트 연결.
  	frontSocket.emit("answer", answer, roomName);//10단계 : answer보내기
});// -> 손님쪽 브라우저에서 돌아가는 코드

frontSocket.on("answer",(answer) => {
    myPeerConnection.setRemoteDescription(answer);//11단계: 주인장 remote연결
});

frontSocket.on("ice", candidate => {
    myPeerConnection.addIceCandidate(candidate);//14단계 : 두 브라우저가 보낸 candidate 서로 받기
})

//RTC Code
function makeConnection(){
    myPeerConnection = new RTCPeerConnection();// 1단계 : 두 브라우저 사이에 peer connection 만듦
  	myPeerConnection.addEventListener("icecandidate", handleIce);//12단계 : IceCandidate 생성
  	myPeerConnection.addEventListener("addStream", handleAddStream); //15 단계: 상대방 stream add.
    //console.log(myStream.getTracks()); //-> video & Audio Tracks가 담겨있음. 즉 우리 stream의 데이터임.
    myStream.getTracks().forEach((track) => myPeerConnection.addTrack(track, myStream));//2단계: 내 stream데이터(비디오, 오디오 데이터)를 peer연결에 넣어주는 것임
}

function handleIce(data){
    //console.log(data); -> candidate찾으삼
    frontSocket.emit("ice", data.candidate , roomName);// 13단계 : IceCandidate를 다른 브라우저로 보내기 위해 서버로 보내기 -> 두 브라우저에게 동시에 적용되는 코드임. 동시에 두 브라우저가 서로에게 data.candidate를 보냄
}

function handleAddStream(data){//15 단계: 상대방 stream add하기 및 media 불러오기
    //console.log(data.stream);// <- 상대 브라우저 stream
    //console.log(myStream);// <- 내 stream
    const peerFace = document.getElementById("peerFace");
    peerFace.srcObject = data.stream;   
}

backend

  1. 1단계: frontend에서 받은 offer 다시 방으로 돌려줌으로써 방금 들어온 브라우저에게 전달하기.
  2. 2단계: frontend에서 받은 answer 다시 방으로 돌려줌으로써 주인 브라우저에게 전달하기.
  3. 3단계 : frontend에서 받은 candidate 다시 방으로 돌려줌으로써 상대 브라우저에게 전달.
  • server.js
//추가
backSocket.on("offer",(offer, roomName) => {
        backSocket.to(roomName).emit("offer", offer);
    });

//최종 코드
ioServer.on("connection",(backSocket) => {
    backSocket.on("join_room", (roomName) =>{
        backSocket.join(roomName);
        backSocket.to(roomName).emit("welcome");
    });
    backSocket.on("offer",(offer, roomName) => {
        backSocket.to(roomName).emit("offer", offer);
    });//1단계
  	backSocket.on("answer",(answer, roomName) => {
        backSocket.to(roomName).emit("answer",answer);
    });//2단계
  	backSocket.on("ice" ,(cnadidate, roomName) => {
        backSocket.to(roomName).emit("ice", cnadidate);
    })//3단계
});

------최종 코드(버그 수정 반영 안됨)-----

  • app.js

import io from "socket.io";

const frontSocket = io();//backend Socket과 연결됨.

const myFace = document.getElementById("myFace");
const muteBtn = document.getElementById("mute");
const cameraBtn = document.getElementById("camera");
const camerasSelect = document.getElementById("cameras");
const call = document.getElementById("call");

call.hidden = true;

let myStream;
let muted = false;
let cameraOff = false;
let roomName;
let myPeerConnection;

async function getCameras(){
    try{
        const devices = await navigator.mediaDevices.enumerateDevices();
        //console.log(devices); -> 확인해보삼
        const cameras = devices.filter((device) => device.kind ===  "videoinput");//true인 애들만 뽑아서 새 array만듦.
        //console.log(cameras); -> videoinput만 있는 새 array 만들어짐.
        const currentCamera = myStream.getVideoTracks()[0];
        cameras.forEach((camera) => {
            const option = document.createElement("option");
            option.value = camera.deviceId;
            option.innerText = camera.label;
            if(currentCamera.label === camera.label){
                option.selected = true;
            }
            camerasSelect.appendChild(option);
        })
    } catch(error){
        console.log(error);
    }
}

async function getMedia(deviceId){
    const initialConstrains = {
        audio: true,
        video: { facingMode : "user" },
    };
    const cameraConstrains = {
        audio: true,
        video: { deviceId: { exact: deviceId } },
    }
    try{
        myStream = await navigator.mediaDevices.getUserMedia(
            deviceId? cameraConstrains : initialConstrains
        );//user의 media를 가져옴(우린 특정 값을 줘서 카메라와 오디오를 가져옴)
        myFace.srcObject = myStream;
        if(!deviceId){
            await getCameras();
        }// deviceId가 없다면 카메라 목록을 가져와라! -> if문 없이 하면 카메라 바꿀 때마다 이 gerMedia함수가 계속 실행되니까 목록이 늘어남
    } catch(error){
        console.log(error)
    }
};


function handleMuteClick(){
    //console.log(myStream.getAudioTracks());//-> track정보(inspect) 제공
    myStream.getAudioTracks().forEach((track) => (track.enabled = !track.enabled));
    if(!muted){//muted = true라면 - default와 반대되는 값이라면// !: 반대되는 값을 리턴해줌
        muteBtn.innerText = "Unmute";
        muted = true;
    } else {
        muteBtn.innerText = "Mute";
        muted = false;
    }
}
function handleCameraClick(){
    //console.log(myStream.getVideoTracks()); -> track정보(inspect) 제공
    myStream.getVideoTracks().forEach((track) => (track.enabled = !track.enabled));
    if(cameraOff){//cameraOff가 참일 때
        cameraBtn.innerText = "Turn Camera Off";
        cameraOff = false;
    } else{
        cameraBtn.innerText = "Turn Camera On";
        cameraOff = true;
    }
}

async function handleCameraChange(){
    await getMedia(camerasSelect.value);
}

muteBtn.addEventListener("click", handleMuteClick);
cameraBtn.addEventListener("click", handleCameraClick);
camerasSelect.addEventListener("input", handleCameraChange);


//Welcome Form (join a room)

const welcome = document.getElementById("welcome");
const welcomeFoam = welcome.querySelector("form");

async function initCall(){
    welcome.hidden = true;
    call.hidden = false;
    await getMedia();
    makeConnection();
}

async function handleWelcomSubmit(event){
    event.preventDefault();
    const input = welcomeFoam.querySelector("input");
    await initCall();//-> 원래 join_room emit에 있었음. 근데 유저가 방에 들어오고 이 함수가 실행되면 처리 속도가 너무 빨라서 뒤에 들어온 손님 유저가 offer를 받지 못해서 이렇게 밖으로 빼줌.
    frontSocket.emit("join_room", input.value);
    roomName = input.value;
    input.value="";
}

welcomeFoam.addEventListener("submit", handleWelcomSubmit);

//Socket Code

frontSocket.on("welcome", async () => {
    const offer = await myPeerConnection.createOffer();//3단계 : offer(초대코드) 만들기 -> 이미 들어와있던 사람에게만 작용하는 코드임.
    myPeerConnection.setLocalDescription(offer);//4단계: 주인장 쪽 콘센트 연결. offer로 연결 구성. -> 이미 들어와있던 사람에게만 작용하는 코드임.
    //console.log(offer);
    frontSocket.emit("offer",offer,roomName);//5단계: offer 보내기
});// -> 주인쪽 브라우저에서 돌아가는 코드

frontSocket.on("offer", async(offer) => {//6단계: offer받기
    myPeerConnection.setRemoteDescription(offer);//7단계: 손님쪽 remote 콘센트 연결
    const answer = await myPeerConnection.createAnswer();// 8단계: answer 생성.
    //console.log(answer);
    myPeerConnection.setLocalDescription(offer);//9단계 : 손님쪽 local 콘센트 연결.
    frontSocket.emit("answer", answer, roomName);//10단계 : 주인장한테 answer보내기
});// -> 손님쪽 브라우저에서 돌아가는 코드

frontSocket.on("answer",(answer) => {
    myPeerConnection.setRemoteDescription(answer);//11단계 : 주인장 remote연결
});

frontSocket.on("ice", candidate => {
    myPeerConnection.addIceCandidate(candidate);//14단계 : 두 브라우저가 보낸 candidate 서로 받기
})

//RTC Code

function makeConnection(){
    myPeerConnection = new RTCPeerConnection();// 1단계: 두 브라우저 사이에 peer connection 만듦
    myPeerConnection.addIceCandidate("icecandidate", handleIce);//12단계 : IceCandidate 생성
    myPeerConnection.addEventListener("addStream", handleAddStream);//15 단계: 상대방 stream add하기 및 media 불러오기
    //console.log(myStream.getTracks()); //-> video & Audio Tracks가 담겨있음. 즉 우리 stream의 데이터임.
    myStream.getTracks().forEach((track) => myPeerConnection.addTrack(track, myStream));//2단계 : 내 stream데이터를 peer연결에 넣어주는 것임
}
function handleIce(data){
    //console.log(data); -> candidate찾으삼
    frontSocket.emit("ice", data.candidate , roomName);// 13단계 : IceCandidate를 다른 브라우저로 보내기 위해 서버로 보내기 -> 두 브라우저에게 동시에 적용되는 코드임. 동시에 두 브라우저가 서로에게 data.candidate를 보냄
}

function handleAddStream(data){//15 단계: 상대방 stream add하기 및 media 불러오기
    //console.log(data.stream);// <- 상대 브라우저 stream
    //console.log(myStream);// <- 내 stream
    const peerFace = document.getElementById("peerFace");
    peerFace.srcObject = data.stream;   
}
  • server.js

import http from "http";
import SocketIO from "socket.io";
import { instrument } from "@socket.io/admin-ui";
import express from "express";


const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views"); // 현재 실행중인 폴더 경로 = process.cwd()와 같다고 볼 수 있음. __dirname : returns the directory name of the directory containing the JavaScript source code file// process.cwd(): returns the current working directory, 즉, node명령을 호출한 디렉토리입니다.
app.use("/public", express.static(__dirname + "/public"));//이미지, CSS 파일 및 JavaScript 파일과 같은 정적 파일을 제공하려면 Express의 기본 제공 미들웨어 함수인 express.static을 사용하십시오. -> app.use('/static', express.static(__dirname + '/public')); 이제 /static 경로 접두부를 통해 public 디렉토리에 포함된 파일을 로드할 수 있습니다.// 정적 파일이란, 직접 값에 변화를 주지 않는 이상 변하지 않는 파일을 의미합니다. 예를 들면, image, css 파일, js 파일 등을 의미합니다. -> static: 정적 파일만을 제공////보안상 유저는 서버 내 모든 폴더를 전부 들여다볼 수 없음(보안상의 이유 때문에) 그래서 유저가 볼 수 있는 폴더를 따로 지정해야댐. 즉, 이건 Front-end폴더임.
app.get("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));// -> catchall : 어떤 url을 가든 "/"로 돌아오게 만들어 home.pug만 볼 수 있도록 하기. 이번 프로젝트에서는 하나의 url만 사용할 것이기 때문에 이렇게 처리해줌.

const httpServer = http.createServer(app);//http 서버 만들기.
const ioServer = SocketIO(httpServer);

ioServer.on("connection",(backSocket) => {
    backSocket.on("join_room", (roomName) =>{
        backSocket.join(roomName);
        backSocket.to(roomName).emit("welcome");
    });
    backSocket.on("offer",(offer, roomName) => {
        backSocket.to(roomName).emit("offer", offer);
    });
    backSocket.on("answer",(answer, roomName) => {
        backSocket.to(roomName).emit("answer",answer);
    });
    backSocket.on("ice" ,(cnadidate, roomName) => {
        backSocket.to(roomName).emit("ice", cnadidate);
    })
});

const handleListen = () => console.log(`Listening on http://localhost:3000`);
httpServer.listen(3000,handleListen);
profile
github: https://github.com/WKlee0607

0개의 댓글