7/23 Web RTC(2)

JK·2023년 7월 23일
0

오늘은 노마드 코더 강의를 보며 Web RTC 구현을 했습니다
강의 링크

Web RTC

서버 코드

import http from "http";
import SocketIO from "socket.io";
import express from "express";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));
app.get("/", (_, res) => res.render("home"));
app.get("/*", (_, res) => res.redirect("/"));

const httpServer = http.createServer(app);
const wsServer = SocketIO(httpServer);

wsServer.on("connection", (socket) => {
  socket.on("join_room", (roomName) => {
    socket.join(roomName);
    socket.to(roomName).emit("welcome");
  });
  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);
  });
});

const handleListen = () => console.log(`Listening on http://localhost:6000`);
httpServer.listen(6000, handleListen);

Socket.IO와 Express를 사용하여 웹 소켓 기능을 구현하는 서버 코드입니다. 이 서버는 사용자들이 영상 통화를 할 수 있는 간단한 채팅 애플리케이션을 만들기 위해 설계되었습니다. 각 부분별로 기능을 설명하겠습니다.

1. 웹 서버 설정:

  • Express 앱을 생성하고, 뷰 엔진으로 Pug를 사용하도록 설정합니다.

  • 정적 파일들을 제공하기 위해 /public 경로를 정적 파일 디렉토리로 설정합니다.

  • 루트 경로("/")로 접속 시, "home" 템플릿을 렌더링하여 클라이언트에게 보여줍니다.

  • 그 외의 모든 경로에 접속 시, 루트 경로("/")로 리다이렉트 합니다.

2. HTTP 서버와 웹 소켓 서버 생성:

  • HTTP 서버를 생성하고, Express 앱을 HTTP 서버와 연결합니다.

  • Socket.IO 라이브러리를 사용하여 웹 소켓 서버를 생성합니다.

3. 웹 소켓 이벤트 핸들링:

  • 클라이언트가 소켓에 접속(connect)하면, "connection" 이벤트가 발생하고 해당 소켓 객체를 인자로 받아 실행됩니다.

  • 클라이언트가 특정 방(room)에 입장(join_room) 요청을 보내면, 해당 방에 입장(join)하도록 합니다. 그리고 해당 방에 있는 다른 클라이언트들에게 "welcome" 이벤트를 보내 환영 메시지를 전달합니다.

  • "offer" 이벤트는 클라이언트가 원격 피어에게 offer 메시지를 보낼 때 발생하며, 해당 방에 있는 다른 클라이언트들에게 offer 메시지를 전달합니다.

  • "answer" 이벤트는 클라이언트가 원격 피어에게 answer 메시지를 보낼 때 발생하며, 해당 방에 있는 다른 클라이언트들에게 answer 메시지를 전달합니다.

  • "ice" 이벤트는 클라이언트가 원격 피어에게 ICE (Interactive Connectivity Establishment) 메시지를 보낼 때 발생하며, 해당 방에 있는 다른 클라이언트들에게 ICE 메시지를 전달합니다.


클라이언트 코드

const socket = io();

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();
    const cameras = devices.filter((device) => device.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;
      }
      camerasSelect.appendChild(option);
    });
  } catch (e) {
    console.log(e);
  }
}

async function getMedia(deviceId) {
  const initialConstrains = {
    audio: true,
    video: { facingMode: "user" },
  };
  const cameraConstraints = {
    audio: true,
    video: { deviceId: { exact: deviceId } },
  };
  try {
    myStream = await navigator.mediaDevices.getUserMedia(
      deviceId ? cameraConstraints : initialConstrains
    );
    myFace.srcObject = myStream;
    if (!deviceId) {
      await getCameras();
    }
  } catch (e) {
    console.log(e);
  }
}

function handleMuteClick() {
  myStream
    .getAudioTracks()
    .forEach((track) => (track.enabled = !track.enabled));
  if (!muted) {
    muteBtn.innerText = "Unmute";
    muted = true;
  } else {
    muteBtn.innerText = "Mute";
    muted = false;
  }
}
function handleCameraClick() {
  myStream
    .getVideoTracks()
    .forEach((track) => (track.enabled = !track.enabled));
  if (cameraOff) {
    cameraBtn.innerText = "Turn Camera Off";
    cameraOff = false;
  } else {
    cameraBtn.innerText = "Turn Camera On";
    cameraOff = true;
  }
}

async function handleCameraChange() {
  await getMedia(camerasSelect.value);
  if (myPeerConnection) {
    const videoTrack = myStream.getVideoTracks()[0];
    const videoSender = myPeerConnection
      .getSenders()
      .find((sender) => sender.track.kind === "video");
    videoSender.replaceTrack(videoTrack);
  }
}

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

// Welcome Form (join a room)

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

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

async function handleWelcomeSubmit(event) {
  event.preventDefault();
  const input = welcomeForm.querySelector("input");
  await initCall();
  socket.emit("join_room", input.value);
  roomName = input.value;
  input.value = "";
}

welcomeForm.addEventListener("submit", handleWelcomeSubmit);

// Socket Code

socket.on("welcome", async () => {
  const offer = await myPeerConnection.createOffer();
  myPeerConnection.setLocalDescription(offer);
  console.log("sent the offer");
  socket.emit("offer", offer, roomName);
});

socket.on("offer", async (offer) => {
  console.log("received the offer");
  myPeerConnection.setRemoteDescription(offer);
  const answer = await myPeerConnection.createAnswer();
  myPeerConnection.setLocalDescription(answer);
  socket.emit("answer", answer, roomName);
  console.log("sent the answer");
});

socket.on("answer", (answer) => {
  console.log("received the answer");
  myPeerConnection.setRemoteDescription(answer);
});

socket.on("ice", (ice) => {
  console.log("received candidate");
  myPeerConnection.addIceCandidate(ice);
});

// RTC Code

function makeConnection() {
  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",
        ],
      },
    ],
  });
  myPeerConnection.addEventListener("icecandidate", handleIce);
  myPeerConnection.addEventListener("addstream", handleAddStream);
  myStream
    .getTracks()
    .forEach((track) => myPeerConnection.addTrack(track, myStream));
}

function handleIce(data) {
  console.log("sent candidate");
  socket.emit("ice", data.candidate, roomName);
}

function handleAddStream(data) {
  const peerFace = document.getElementById("peerFace");
  peerFace.srcObject = data.stream;
}

이 코드는 앞서 설명한 서버와 클라이언트 사이에서 실시간 영상 통화를 수행하는 클라이언트 측의 JavaScript 코드입니다. 코드의 기능을 다음과 같이 설명하겠습니다.

1. 카메라 및 미디어 설정 관련 함수:

  • getCameras(): 미디어 장치(카메라) 목록을 가져와서 옵션으로 추가하고, 현재 선택된 카메라를 표시합니다.

  • getMedia(deviceId): 사용자의 미디어(오디오 및 비디오)를 가져옵니다. deviceId가 주어지면 해당 카메라를 사용하고, 없으면 기본 카메라를 사용합니다.

2. 뮤트(Mute) 및 카메라 On/Off 버튼 관련 함수:

  • handleMuteClick(): 오디오 뮤트 버튼을 클릭하면 오디오 트랙의 활성화 여부를 변경하고, 상태에 따라 버튼의 텍스트를 변경합니다.

  • handleCameraClick(): 카메라 On/Off 버튼을 클릭하면 비디오 트랙의 활성화 여부를 변경하고, 상태에 따라 버튼의 텍스트를 변경합니다.

  • handleCameraChange(): 사용자가 다른 카메라를 선택할 때 호출되며, 선택한 카메라로 미디어를 갱신합니다.

3.환영 메시지 폼과 이벤트 핸들러:

  • handleWelcomeSubmit(event): 사용자가 방 이름을 입력하고 제출하면 initCall() 함수를 호출하여 영상 통화를 시작하고, 소켓을 통해 방에 입장을 알립니다.

4.Socket.IO 이벤트 핸들러:

  • socket.on("welcome", ...): 서버로부터 "welcome" 이벤트를 수신하면 myPeerConnection.createOffer()를 호출하여 오퍼(offer)를 생성하고, 생성한 오퍼를 로컬 디스크립션으로 설정한 후 소켓을 통해 서버에 전송합니다.

  • socket.on("offer", ...): 서버로부터 "offer" 이벤트를 수신하면 해당 오퍼를 받아와서 원격 디스크립션으로 설정한 후, myPeerConnection.createAnswer()를 호출하여 앤서(answer)를 생성하고, 앤서를 로컬 디스크립션으로 설정한 후 소켓을 통해 서버에 전송합니다.

  • socket.on("answer", ...): 서버로부터 "answer" 이벤트를 수신하면 해당 앤서를 받아와서 원격 디스크립션으로 설정합니다.

  • socket.on("ice", ...): 서버로부터 "ice" 이벤트를 수신하면 해당 ICE(Interactive Connectivity Establishment) 정보를 받아서 피어 연결에 추가합니다.

5. WebRTC (RTCPeerConnection) 관련 함수:

  • makeConnection(): RTCPeerConnection을 생성하고, ICE 서버 정보를 설정하며, icecandidate 이벤트와 addstream 이벤트를 등록합니다. 또한 사용자의 미디어 트랙을 피어 연결에 추가합니다.

  • handleIce(data): ICE 후보(candidate) 정보를 소켓을 통해 서버에 전송합니다.

  • handleAddStream(data): 피어로부터 스트림이 추가되었을 때 실행되며, 피어의 비디오를 video 요소에 표시합니다.

profile
^^

0개의 댓글

관련 채용 정보