Zoom Clone Coding part 4
Zoom Clone Coding
Web RTC Structure
Web RTC는 web real-time commucation 방식의 기술로 연결되어 있는 유저 끼리 비디오, 오디오, 등의 영상 미디어를 전송할 수 있다. 기존에 쓴 socket.io 방식과의 차이점은 WebRTC는 peer-to-peer 방식이라는 점이다. peer 간에 통신을 진행할 때 서버를 거치지 않고 바로 통신이 가능하기 때문에 그 만큼 latency가 작다는 장점이 있다.
다만, signaling process 과정에서는 socket을 통한 configuration 과정이 필요하고, 각각의 peer에 어디에 있는 지 알기 위해 server가 필요하며, 한 번 연결설정이 완료되면 그 이후에는 server가 필요하지 않다.
Web RTC의 signaling process은 위와 같이 Offer, Answer, ICE-Candidate, Add Stream으로 이루어져 있다, 위 그림의 Peer A, Peer B를 이용해서 해당 과정을 이해보자.
RTCPeerConnection 생성
브라우져의 Media Stream을 받아오고 난 다음, peer-peer 간에 connection을 관장하는 RTCPeerConnection을 생성해서 브라우져에서 유지한다.
app.js
let myPeerConnection
async function startMedia(){
welcome.hidden=true;
call.hidden=false;
await getMedia();
makeConnection();
}
function makeConnection(){
myPeerConnection=new RTCPeerConnection();
//add Tracks 대신에 사용하는 함수, myStream은 이전에 생성한 MediaStream
myStream.getTracks().forEach(
track=> myPeerConnection.addTrack(track,myStream)
);
}
Offer
연결하는 브라우져 쪽에서는 Offer을 생성하고, 이를 Local Description에 등록해야한다.
home.pug
div#welcome
form
input(placeholder="room name",required,type="text")
button Enter Room
app.js
const welcome=document.getElementById("welcome");
const welcomeForm=welcome.querySelector("form");
async function handleWelcomeSubmit(event){
event.preventDefault();
const input=welcomeForm.querySelector("input");
console.log(input.value);
await startMedia();
socket.emit("join_room",input.value);
roomName=input.value;
input.value="";
}
welcomeForm.addEventListener("submit",handleWelcomeSubmit)
Front 쪽에서는 room에 접속하겠다 ==> 연결을 하겠다 라고 생각하게 된다. 이때, 서버로 “join_room” event을 보내서 본격적으로 signaling process 작업을 진행한다.
server.js
socket.on("join_room",(roomName) => {
socket.join(roomName);
socket.to(roomName).emit("welcome");
})
app.js
socket.on("welcome",async ()=> { const offer=await myPeerConnection.createOffer(); myPeerConnection.setLocalDescription(offer) socket.emit("offer",offer,roomName); })
먼저 연결하고자 하는 Peer A 쪽에서는 offer을 생성하고, 이를 본인의 RTCPeerConnection의 LocalDescription에 등록하고, 이를 Peer B로 넘기기 위해 우선, 서버로 보낸다. AddStream은 과정은 myPeerConnection 생성을 담당하는 function에서 수행해주고 있다.
Answer
server.js
//offer
socket.on("offer",(offer,roomName) =>{
socket.to(roomName).emit("offer",offer);
});
서버에서는 offer을 받으면 Peer B에게 넘겨준다.
app.js
socket.on("offer",async (offer)=>{
myPeerConnection.setRemoteDescription(offer);
const answer=await myPeerConnection.createAnswer();
myPeerConnection.setLocalDescription(answer);
socket.emit("answer",answer,roomName);
})
Offer을 받은 Peer B쪽에서는 Answer을 준비해서 Peer A에게 전달해야한다. 전달받은 offer을 자신의 RTCPeerConnection의 RemoteDescription으로 등록하고, Answer 객체를 생성한다. 생성한 Answer 객체는 RTCPeerConnection의 LocalDescription으로 등록하고 이를 Peer A로 보낸다.
server.js
socket.on("answer",(answer,roomName)=>{
socket.to(roomName).emit("answer",answer);
})
app.js
socket.on("answer",answer=>{
myPeerConnection.setRemoteDescription(answer);
})
그리고 Peer A에서는 전달받은 Answer을 자신의 RemoteDescription으로 등록해야한다.
ICE Candidate
ICE 는 Internet Connectivity Establishment의 약자로 통신 간 운용할 수 있는 protocol들을 명시한 것이다. 각각의 Peer는 ICE Candidate을 서로의 원격 peer에게 전달해야한다. ICE Candidate는 RTCPeerConnection에서 자동적으로 보내기 때문에 이에 대한 event handler function을 등록시켜주기만 하면 된다.
app.js
function makeConnection(){
myPeerConnection=new RTCPeerConnection();
myPeerConnection.addEventListener("icecandidate",handleIce)
//add Tracks 대신에 사용하는 함수
myStream.getTracks().forEach(
track=> myPeerConnection.addTrack(track,myStream)
);
function handleIce(data){
socket.emit("ice",data.candidate,roomName);
}
IceCandidate event가 발생할때마다 브라우져는 이를 서로의 peer에게 전달하게 된다.
server.js
socket.on("ice",(ice,roomName)=>{
socket.to(roomName).emit("ice",ice);
})
AddStreamEvent
Stream event을 받게 되면 이는 상대방의 Stream을 정상적으로 받았다는 것을 의미하며 이에 대해서 적절히 처리를 해준다.
app.js
function makeConnection(){
myPeerConnection=new RTCPeerConnection();
myPeerConnection.addEventListener("icecandidate",handleIce)
myPeerConnection.addEventListener("addstreamevent",handleAddStream);
//add Tracks 대신에 사용하는 함수
myStream.getTracks().forEach(
track=> myPeerConnection.addTrack(track,myStream)
);
}
//받아온 Stream을 등록하면 상대방의 stream이 등록된다.
function handleAddStream(data){
const peerFace=document.getElementById("peerFace");
peerFace.srcObject=data.stream;
}
Camera Switch
본인이 이용하고 있는 카메라를 수정하게 되면, 본인의 화면은 변하지만, 상대방 측의 화면에서는 해당 변화가 발생하지 않는다. 이는 Stream 변화가 RTCPeerConnection에 정상적으로 반영되지 않기 때문이다. 따라서, Stream의 변화가 생길 떄 이를 RTCPeerConnection에 새롭게 등록시켜준다.
app.js
async function handleCameraSelectChange(){
await getMedia(camerasSelect.value);
//need to change new stream to peerConnection
if(myPeerConnection){
const videoSender=myPeerConnection
.getSenders()
.find(sender=> sender.track.kind="vide");
videoSender.replaceTrack(myStream.getVideoTracks()[0])
}
}
카메라를 수정했을 때 실행되는 event handler function에서 RTCPeerConnection에 새로운 Strea을 등록시켜주는 작업을 진행한다. 이때, Senders를 통해 수정을 진행하는데, Senders는 RTCPeerConnection에서 mediaStream Track를 담당하는 모듈이다.
LT & Stun Server
Local Tunnel 기술을 활용하게 되면 일시적으로 url로 접속할 수 있도록 허용해준다. 이를 통해 스마트폰을 통한 앱 실행이 가능하다.
하지만, 같은 Wifi 연결이 아닐때는 각각의 Peer의 공인 IP를 찾기 어렵다 –> 이때 Stun Server을 통해 공인 IP를 쉽게 찾아낼 수 있다.
app.js
myPeerConnection=new RTCPeerConnection({
iceServers:[
{
urls:[
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302",
"stun:stun2.l.google.com:19302",
"stun:stun3.l.google.com:19302",
"stun:stun4.l.google.com:19302",
]
}
]
}
);
이렇게 무료로 제공해주는 stun-server을 활용하면 된다.
Data Channel
Data Channel을 통해 peer는 서로에게 message, video, file, 등의 데이터를 주고 받을 수 있다.
Peer A 에서 Data Channel을 생성하여 이를 Peer B에게 전달하고, Peer B 는 전달받은 Data Channel을 유지한다.
app.js
// Peer A 쪽 실행
socket.on("welcome",async ()=>{
myDataChannel=myPeerConnection.createDataChannel("chat");
myDataChannel.addEventListener("message",console.log);
//Peer B에서 실행
socket.on("offer",async (offer)=>{
myPeerConnection.addEventListener("datachannel",(event)=>{
myDataChannel=event.channel;
myDataChannel.addEventListener("message",console.log);
});
References
Link: nomadcoders
Link: WebRTC
댓글남기기