먼저 PeerA는
1. 브라우저에서 미디어 스트림을 받는다.(getUserMedia)
2. stream을 등록한다.(addTrack)
3. createOffer 후에 local sdp를 설정한다. (createOffer => setLocalDescription)
4. PeerB에 offer을 전달한다. (send offer)
PeerB에서는 offer을 받으면
5. PeerA에게서 받은 offer(sdp)로 remote sdp를 설정한다. (setRemoteDescription)
6. 브라우저 미디어 스트림을 받는다. (getUserMedia)
7. createAnswer 후 local sdp 설정한다. (createAnswer => setLocalDescription)
8. PeerA에게 answer을 보낸다. (send answer)
9. PeerA에서는 answer를 전달받고 remote sdp를 설정한다. (setRemoteDescription)
10. create-answer 과정이 끝나면 icecandidate로 네트워크 정보를 교환한다.
이렇게 해서 두 피어간의 연결이 완료된다.
const express = require("express");
const app = express();
const http = require("http");
const { Server } = require("socket.io");
const server = http.createServer(app);
// cors 설정을 하지 않으면 오류가 생기게 된다.
const io = new Server(server, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"],
allowedHeaders: ["my-custom-header"],
credentials: true,
},
});
const PORT = process.env.PORT || 8080;
// 어떤 방에 어떤 유저가 들어있는지
let users = {};
// socket.id 기준으로 어떤 방에 들어있는지
let socketRoom = {};
// 방의 최대 인원수
const MAXIMUM = 2;
io.on("connection", (socket) => {
console.log(socket.id, "connection");
socket.on("join_room", (data) => {
// 방이 기존에 생성되어 있다면
if (users[data.room]) {
// 현재 입장하려는 방에 있는 인원수
const currentRoomLength = users[data.room].length;
if (currentRoomLength === MAXIMUM) {
// 인원수가 꽉 찼다면 돌아간다.
socket.to(socket.id).emit("room_full");
return;
}
// 여분의 자리가 있다면 해당 방 배열에 추가한다.
users[data.room] = [...users[data.room], { id: socket.id }];
} else {
// 방이 존재하지 않는다면 값을 생성하고 추가한다.
users[data.room] = [{ id: socket.id }];
}
socketRoom[socket.id] = data.room;
// 입장
socket.join(data.room);
// 입장하기 전 해당 방의 다른 유저들이 있는지 확인하고
// 다른 유저가 있었다면 offer-answer을 위해 알려준다.
const others = users[data.room].filter((user) => user.id !== socket.id);
if (others.length) {
io.sockets.to(socket.id).emit("all_users", others);
}
});
socket.on("offer", (sdp, roomName) => {
// offer를 전달받고 다른 유저들에게 전달한다.
socket.to(roomName).emit("getOffer", sdp);
});
socket.on("answer", (sdp, roomName) => {
// answer를 전달받고 방의 다른 유저들에게 전달한다.
socket.to(roomName).emit("getAnswer", sdp);
});
socket.on("candidate", (candidate, roomName) => {
// candidate를 전달받고 방의 다른 유저들에게 전달한다.
socket.to(roomName).emit("getCandidate", candidate);
});
socket.on("disconnect", () => {
// 방을 나가게 된다면 socketRoom과 users의 정보에서 해당 유저를 지운다.
const roomID = socketRoom[socket.id];
if (users[roomID]) {
users[roomID] = users[roomID].filter((user) => user.id !== socket.id);
if (users[roomID].length === 0) {
delete users[roomID];
return;
}
}
delete socketRoom[socket.id];
socket.broadcast.to(users[roomID]).emit("user_exit", { id: socket.id });
});
});
server.listen(PORT, () => {
console.log(`server running on ${PORT}`);
});
import { useRef } from "react";
import { useParams } from "react-router-dom";
import { Socket } from "socket.io-client";
const VideoCall = () => {
// 소켓정보를 담을 Ref
const socketRef = useRef<Socket>();
// 자신의 비디오
const myVideoRef = useRef<HTMLVideoElement>(null);
// 다른사람의 비디오
const remoteVideoRef = useRef<HTMLVideoElement>(null);
// peerConnection
const pcRef = useRef<RTCPeerConnection>();
const {roomName} = useParams();
useEffect(() => {
// 소켓 연결
socketRef.current = io("localhost:3000");
// peerConnection 생성
// iceServers는 stun sever설정이며 google의 public stun server를 사용
peerRef.current = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.l.google.com:19302",
},
],
});
}, [])
return (
<div>
<video ref={myVideoRef} autoPlay />
<video ref={remoteVideoRef} autoPlay />
</div>
);
};
export default VideoCall;
const getMedia = async () => {
try {
// 자신이 원하는 자신의 스트림정보
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
if(myVideoRef.current){
myVideoRef.current.srcObject = stream
}
// 스트림을 peerConnection에 등록
stream.getTracks().forEach((track) => {
if (!peerRef.current) {
return;
}
peerRef.current.addTrack(track, stream);
});
// iceCandidate 이벤트
peerRef.current.onicecandidate = (e) => {
if (e.candidate) {
if (!socketRef.current) {
return;
}
console.log("recv candidate");
socketRef.current.emit("candidate", e.candidate, roomName);
}
};
// addTrack 이벤트
peerRef.current.ontrack = (e) => {
if (otherVideoRef.current) {
otherVideoRef.current.srcObject = e.streams[0];
}
};
} catch (e) {
console.error(e)
}
}
const createOffer = async () => {
console.log("create Offer");
if (!(peerRef.current && socketRef.current)) {
return;
}
try {
// offer 생성
const sdp = await peerRef.current.createOffer();
// 자신의 sdp로 LocalDescription 설정
peerRef.current.setLocalDescription(sdp);
console.log("sent the offer");
// offer 전달
socketRef.current.emit("offer", sdp, roomName);
} catch (e) {
console.error(e);
}
};
const createAnswer = async (sdp: RTCSessionDescription) => {
// sdp : PeerA에게서 전달받은 offer
console.log("createAnswer");
if (!(peerRef.current && socketRef.current)) {
return;
}
try {
// PeerA가 전달해준 offer를 RemoteDescription에 등록한다.
peerRef.current.setRemoteDescription(sdp);
// answer를 생성하고
const answerSdp = await peerRef.current.createAnswer();
// answer를 LocalDescription에 등록한다. (PeerB 기준)
peerRef.current.setLocalDescription(answerSdp);
console.log("sent the answer");
socketRef.current.emit("answer", answerSdp, roomName);
} catch (e) {
console.error(e);
}
};
useEffect(() => {
socketRef.current = io("localhost:8080");
pcRef.current = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.l.google.com:19302",
},
],
});
// 기존 유저가 있고, 새로운 유저가 들어왔다면 오퍼 생성
socketRef.current.on("all_users", (allUsers: Array<{ id: string }>) => {
if (allUsers.length > 0) {
createOffer();
}
});
// offer를 전달받은 PeerB만 해당된다
// offer를 들고 만들어둔 answer 함수 실행
socketRef.current.on("getOffer", (sdp: RTCSessionDescription) => {
console.log("recv Offer");
createAnswer(sdp);
});
// answer를 전달받을 PeerA만 해당된다
// answer를 전달받아 PeerA의 RemoteDescription에 등록
socketRef.current.on("getAnswer", (sdp: RTCSessionDescription) => {
console.log("recv Answer");
if (!pcRef.current) {
return;
}
pcRef.current.setRemoteDescription(sdp);
});
// 서로의 candidate를 전달받아 등록
socketRef.current.on("getCandidate", async (candidate: RTCIceCandidate) => {
if (!pcRef.current) {
return;
}
await pcRef.current.addIceCandidate(candidate);
});
// 마운트시 해당 방의 roomName을 서버에 전달
socketRef.current.emit("join_room", {
room: roomName,
});
getMedia();
return () => {
// 언마운트시 socket disconnect
if (socketRef.current) {
socketRef.current.disconnect();
}
if (pcRef.current) {
pcRef.current.close();
}
};
}, []);