
-> 앞으로의 과정: socketIO사용, signaling을 위해 webSocket을 사용하고, peer-to-peer가 되면 이전에 만든 stream을 보내줄 것임.
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
//추가
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();
//추가
ioServer.on("connection",(backSocket) => {
backSocket.on("join_room", (roomName, done) =>{
backSocket.join(roomName);
done();
});
});
//추가
//Socket Code
frontSocket.on("welcome", () => {
console.log("someone joined");
});
//추가
backSocket.to(roomName).emit("welcome");

-> 위의 사진 순서를 따라갈 것임.
-> addStream을 하기 전에, 양쪽 브라우저에 RTC연결을 먼저 해야함. 우선 양쪽 브라우저의 연결을 만들어야함. 양쪽에서 연결통로를 만들어서 그것들을 같이 연결할 것임. 중요한 점은, 이 연결들이 일단, 따로 설정이 이루어질 것이고, 그것들을 (server)socketIO를 이용해서 이어줄 것임.
-> getUserMedia, addStream은 건너뜀. 왜냐하면 이미 손님 유저가 들어올 때 실행되기 때문임.
-> 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을 추가하면 비디오가 나옴.ㄴ
//#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")
//추가 및 수정
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;
}
//추가
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단계
});
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;
}
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);