일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- next js
- pyinstaller
- 비디오스트리밍
- Nodejs
- APIroutes
- 쿠키관리
- 페이스북개발자
- socketIO
- 넥스트js
- 플래닛스케일
- state전역관리
- reactjs
- pyqt5
- mysqlworkbench
- nextjs
- 웹소켓
- PlanetScale
- nextjs13
- ReactQuill
- API루트
- 앱비밀번호
- 비디오전송
- 인스타그램앱만들기
- 노드메일러
- nodemailer
- 인스타그램API
- ReactContextAPI
- 넥스트JS13
- 리액트
- expressjs
- Today
- Total
Timpossible history
[Express JS] Socket.IO로 실시간 화상 채팅 구성(feat. WebRTC) 본문
저번 Socket.IO로 실시간 채팅 웹사이트 구현한 것에 이어서, 실시간 화상 채팅은 어떻게 구성하는가 알아보자.
[Express JS] Socket.IO 이용해서 실시간 채팅 어플리케이션 구성
메인으로 실시간 양방향 통신은 Socket.IO로 하겠지만, 화상 채팅 통신은 WebRTC의 RTCPeerConnection을 이용할 예정이다.
1. 프로젝트 시작
mkdir webrtc-video-chat
cd webrtc-video-chat
npm init -y
먼저 프로젝트를 시작해주고 필요한 패키지를 설치한다.
npm install express socket.io
2. 서버 사이드 설정
// src/server.js
import express from "express";
import socketIO from "socket.io";
import http from "http";
const app = express();
app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));
app.get("/", (req, res) => res.render("video"));
app.get("/*", (req, res) => res.redirect("/"));
const handleListen = () => console.log(`Listening on http://localhost:3000`);
const httpServer = http.createServer(app);
const wsServer = socketIO(httpServer);
wsServer.on("connection", (socket) => {
socket["nickname"] = "Anon";
socket.on("disconnecting", () => {
socket.rooms.forEach((room) =>
socket.to(room).emit("bye", `${socket.nickname} left..`)
);
});
socket.on("disconnect", () => {
console.log('disconnected!');
});
socket.on("join_room", (roomName) => {
socket.join(roomName);
socket.to(roomName).emit("welcome", "someone joined!");
});
socket.on("offer", (offer, roomName) => {
socket.to(roomName).emit("offer", offer);
});
socket.on("answer", (answer, roomName) => {
socket.to(roomName).emit("answer", answer);
});
socket.on("ice", (ice, roomName) => {
socket.to(roomName).emit("ice", ice);
});
});
httpServer.listen(3000, handleListen);
view engine을 pug로 설정해주고, 클라이언트에서 서버로 / 루트 요청이 들어오면 views 폴더에 있는 video.pug 파일을 렌더링하도록 설정해주고, / 루트 외의 모든 루트(/*)로 들어온 요청은 /루트로 리디렉션 하도록 설정한다.
3. 클라이언트 사이드 설정
// /src/views/video.pug
doctype html
html(lang="en")
head
meta(charset="UTF-8")
meta(http-equiv="X-UA-Compatible", content="IE=edge")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title video
link(rel="stylesheet", href="https://unpkg.com/mvp.css@1.12/mvp.css")
body
header
h1 Noom
main
div#welcome
form
input(type="text", placeholder="room name")
button Enter room
div#videoContainer
video#myFace(autoplay, playsinline, width=400, height=400)
select#cameraSelect
// 임의의 option 값을 설정했다
option(value="11") 11
option(value="22") 22
option(value="33") 33
button#audio Mute
button#camera Turn camera on
video#peerFace(autoplay, playsinline, width=400, height=400)
script(src="/socket.io/socket.io.js")
script(src="/public/js/video.js")
유저가 / 루트로 메인페이지에 들어올 때 서버에서 설정해놓은대로, video.pug 파일을 렌더링해서 보여주는데, 아래에 적힌대로 렌더링해줄 것이다.
video.pug에서 실행시킬 js 코드는 video.js를 연결해준다.
// src/public/js/video.js
const socket = io();
const welcome = document.querySelector("#welcome");
const form = welcome.querySelector("form");
const videoContainer = document.querySelector("#videoContainer");
videoContainer.hidden = true;
let roomName;
async function initCall() {
await getVideo();
makeConnection();
videoContainer.hidden = false;
welcome.hidden = true;
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
const input = form.querySelector("input");
await initCall();
socket.emit("join_room", input.value);
roomName = input.value;
input.value = "";
});
const myFace = document.querySelector("#myFace");
const cameraSelect = document.querySelector("#cameraSelect");
const muteBtn = document.querySelector("#audio");
const cameraBtn = document.querySelector("#camera");
let myStream;
let mute = false;
let cameraOff = false;
let peerConnection;
let myDataChannel;
async function getVideo(deviceId) {
const initialConstraints = {
audio: true,
video: { facingMode: "user" },
};
const selectedConstraints = {
audio: true,
video: {
deviceId: {
exact: deviceId,
},
},
};
try {
myStream = await navigator.mediaDevices?.getUserMedia(
deviceId ? selectedConstraints : initialConstraints
);
myFace.srcObject = myStream;
if (!deviceId) {
await getCameras();
}
} catch (e) {
console.log(e);
}
}
async function getCameras() {
try {
const cameras = await navigator.mediaDevices
?.enumerateDevices()
.then((result) =>
result.filter((camera) => camera.kind === "videoinput")
);
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;
}
cameraSelect.appendChild(option);
});
} catch (e) {
console.log(e);
}
}
muteBtn.addEventListener("click", (e) => {
e.preventDefault();
myStream
.getAudioTracks()
.forEach((track) => (track.enabled = !track.enabled));
if (!mute) {
muteBtn.innerText = "Unmute";
} else {
muteBtn.innerText = "Mute";
}
mute = !mute;
});
cameraBtn.addEventListener("click", (e) => {
e.preventDefault();
myStream
.getVideoTracks()
.forEach((track) => (track.enabled = !track.enabled));
if (!cameraOff) {
cameraBtn.innerText = "Turn camera on";
} else {
cameraBtn.innerText = "Turn camera off";
}
cameraOff = !cameraOff;
});
cameraSelect.addEventListener("change", async (e) => {
await getVideo(e.target.value);
if (peerConnection) {
const videoTrack = myStream.getVideoTracks()[0];
const videoSender = peerConnection
.getSenders()
.find((sender) => sender.track.kind === "video");
videoSender.replaceTrack(videoTrack);
}
});
socket.on("welcome", async () => {
myDataChannel = peerConnection.createDataChannel("chat");
myDataChannel.addEventListener("message", console.log);
const offer = await peerConnection.createOffer();
peerConnection.setLocalDescription(offer);
socket.emit("offer", offer, roomName);
console.log("sent the offer");
});
socket.on("offer", async (offer) => {
peerConnection.addEventListener("datachannel", (event) => {
myDataChannel = event.channel;
myDataChannel.addEventListener("message", console.log);
});
console.log("received the offer");
peerConnection.setRemoteDescription(offer);
const answer = await peerConnection.createAnswer();
peerConnection.setLocalDescription(answer);
socket.emit("answer", answer, roomName);
console.log("sent the answer");
});
socket.on("answer", (answer) => {
console.log("received the answer");
peerConnection.setRemoteDescription(answer);
});
socket.on("ice", (ice) => {
peerConnection.addIceCandidate(ice);
});
function makeConnection() {
peerConnection = 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",
],
},
],
});
console.log("made connection");
peerConnection.addEventListener("icecandidate", (data) => {
socket.emit("ice", data.candidate, roomName);
});
peerConnection.addEventListener("track", (data) => {
const peerFace = document.querySelector("#peerFace");
peerFace.srcObject = data.streams[0];
});
// peerConnection.addEventListener("addstream", (data) => {
// const peerFace = document.querySelector("#peerFace");
// peerFace.srcObject = data.stream;
// });
myStream
.getTracks()
.forEach((track) => peerConnection.addTrack(track, myStream));
}
form 태그의 room name을 input태그에 입력하고 enter room 버튼을 누르고 들어간다.
이 때, videoContainer와 form의 parent div의 hidden property를 스위치 해준다.
form 태그의 'submit' 이벤트에 jnitCall() 함수를 등록해준다.
initCall 함수에서는 getVideo()와 makeConnection()이 호출된다.
getVideo 함수는 navigator.mediaDevices.getUsermedia 메서드를 통하여 유저의 카메라에 대한 접근을 허용하는 팝업을 띄워준다. 이 때 유저가 허용을 하게 되면 카메라가 켜지게 된다.
그리고 얻어지는 스트리밍 데이터는 myStream 변수에 넣어주고 myStream의 값은 myFace.srcObject 메서드를 통해서 myFace div에 스트리밍 데이터를 띄워주게 되면 유저의 카메라를 통해 자신의 모습을 볼 수 있게 된다.
'백엔드 > Node JS' 카테고리의 다른 글
[Node JS] Express JS, 서버에서 클라이언트로 동영상 보내기(Feat. Streaming) (0) | 2024.02.29 |
---|---|
[Express JS] Socket.IO 이용해서 실시간 채팅 어플리케이션 구성 (0) | 2024.01.03 |
[Node JS] Nodemailer 사용 시 필요한 앱 비밀번호 설정 (0) | 2023.12.31 |