[project] WebRTC signaling [ 2 ]

😎·2023년 1월 8일
0

PROJECT

목록 보기
11/26
post-thumbnail

이전 포스팅과 이어지는 글입니다.

이전 글에서 테스트가 이루어 지지않은 코드를 올려두었는데요
백엔드 서버와 테스트 한 결과 원하던 결과를 얻지 못했습니다 ㅠㅠㅠ
이후 코드 수정하여 signaling이 성공한 코드를 올려보겠습니다 :)

// 외부모듈
import styled from 'styled-components';
import React, { useRef, useEffect, useState } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { useCookies } from 'react-cookie';
import * as SockJs from 'sockjs-client';
import * as StompJs from '@stomp/stompjs';

// 내부모듈
import { instance } from '../../../../api/core/axios';

function GameRoomAll() {
  const reconnect = 0;
  const videoRef = useRef(null);
  const anotherVideoRef = useRef(null);
  const muteBtn = useRef(null);
  const cameraBtn = useRef(null);
  const camerasSelect = useRef(null);
  const cameraOption = useRef(null);

  const navigate = useNavigate();
  const [cookie] = useCookies();
  const client = useRef({});
  const connectHeaders = {
    Authorization: cookie.access_token,
    'Refresh-Token': cookie.refresh_token,
  };

  const param = useParams();
  const [messages, setMessages] = useState([]);
  const messageArray = [];
  let muted = false;
  let cameraOff = false;
  let stream;
  let myPeerConnection;

  const sender = sessionStorage.getItem('nickname');
  console.log('sender', sender);

  const subscribe = () => {
    client.current.subscribe(
      `/sub/gameroom/${param.roomId}`,
      async ({ body }) => {
        const data = JSON.parse(body);
        // console.log(data);
        switch (data.type) {
          case 'ENTER':
            if (data.sender !== sender) {
              console.log(data);
              const offer = await myPeerConnection.createOffer();
              myPeerConnection.setLocalDescription(offer);
              client.current.publish({
                destination: `/sub/gameroom/${param.roomId}`,
                body: JSON.stringify({
                  type: 'OFFER',
                  roomId: param.roomId,
                  sender,
                  offer,
                }),
              });
              console.log('오퍼전송');
            }
            break;

          case 'OFFER':
            if (data.sender !== sender) {
              console.log('오퍼수신');
              myPeerConnection.setRemoteDescription(data.offer);
              const answer = await myPeerConnection.createAnswer();
              myPeerConnection.setLocalDescription(answer);
              client.current.publish({
                destination: `/sub/gameroom/${param.roomId}`,
                body: JSON.stringify({
                  type: 'ANSWER',
                  roomId: param.roomId,
                  sender,
                  answer,
                }),
              });
              console.log('엔서전송');
            }
            break;
          case 'ANSWER':
            if (data.sender !== sender) {
              console.log('엔서수신');
              myPeerConnection.setRemoteDescription(data.answer);
            }
            break;
          case 'ICE':
            if (data.sender !== sender) {
              console.log('아이스수신');
              myPeerConnection.addIceCandidate(data.ice);
            }
            break;
          default:
        }
      },
    );
  };
  const connect = () => {
    client.current = new StompJs.Client({
      webSocketFactory: () => new SockJs(`http://13.209.84.31:8080/ws-stomp`),
      connectHeaders,
      debug() {},
      onConnect: () => {
        subscribe();
        client.current.publish({
          destination: `/sub/gameroom/${param.roomId}`,
          body: JSON.stringify({
            type: 'ENTER',
            roomId: param.roomId,
            sender,
          }),
        });
      },
      onStompError: (frame) => {
        console.log(`Broker reported error: ${frame.headers.message}`);
        console.log(`Additional details: ${frame.body}`);
      },
    });
    client.current.activate();
  };
  const disconnect = () => {
    client.current.deactivate();
  };
  const leaveRoom = async () => {
    disconnect();
    await instance
      .delete(`rooms/${param.roomId}/exit`)
      .then((res) => {
        navigate('/rooms');
      })
      .catch((error) => {
        alert(error.data.message);
        navigate('/rooms');
      });
  };
  function onClickCameraOffHandler() {
    stream.getVideoTracks().forEach((track) => {
      track.enabled = !track.enabled;
    });
    if (!cameraOff) {
      cameraBtn.current.innerText = 'OFF';
      cameraOff = !cameraOff;
    } else {
      cameraBtn.current.innerText = 'ON';
      cameraOff = !cameraOff;
    }
  }
  function onClickMuteHandler() {
    stream.getAudioTracks().forEach((track) => {
      track.enabled = !track.enabled;
    });
    if (!muted) {
      muteBtn.current.innerText = 'Unmute';
      muted = !muted;
    } else {
      muteBtn.current.innerText = 'Mute';
      muted = !muted;
    }
  }

  async function getCameras() {
    try {
      // 유저의 장치를 얻어옵니다
      const devices = await navigator.mediaDevices.enumerateDevices();
      // 얻어온 유저의 장치들에서 카메라장치만 필터링 합니다
      const cameras = devices.filter((device) => device.kind === 'videoinput');
      // 현재내가 사용중인 카메라의 label명을 셀렉트란에 보여주기위한 과정입니다.
      //  아래의 if문과 이어서 확인 해주세요
      const currentCamera = stream.getVideoTracks()[0];
      cameras.forEach((camera) => {
        cameraOption.current.value = camera.deviceId;
        cameraOption.current.innerText = camera.label;
        if (currentCamera.label === camera.label) {
          cameraOption.current.selected = true;
        }
        camerasSelect.current.appendChild(cameraOption.current);
      });
    } catch (error) {
      console.log(error);
    }
  }

  async function getUserMedia(deviceId) {
    const initialConstrains = {
      video: { facingMode: 'user' },
      audio: true,
    };
    const cameraConstrains = {
      audio: true,
      video: { deviceId: { exact: deviceId } },
    };
    try {
      stream = await navigator.mediaDevices.getUserMedia(
        deviceId ? cameraConstrains : initialConstrains,
      );
      videoRef.current.srcObject = stream;
      if (!deviceId) {
        await getCameras();
      }
    } catch (err) {
      console.log(err);
    }
  }

  async function onInputCameraChange() {
    await getUserMedia(camerasSelect.current.value);
    if (myPeerConnection) {
      const videoTrack = stream.getVideoTracks()[0];
      const videoSender = myPeerConnection
        .getSenders()
        .find((sender) => sender.track.kind === 'video');
      videoSender.replaceTrack(videoTrack);
    }
  }

  function handleIce(data) {
    client.current.publish({
      destination: `/sub/gameroom/${param.roomId}`,
      body: JSON.stringify({
        type: 'ICE',
        roomId: param.roomId,
        sender,
        ice: data.candidate,
      }),
    });
    console.log('아이스전송');
  }

  function handleAddStream(data) {
    anotherVideoRef.current.srcObject = data.stream;
    console.log('got an stream from my peer');
    console.log("Peer's Stream", data.stream);
    console.log('My stream', stream);
  }
  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);
    stream.getTracks().forEach((track) => {
      myPeerConnection.addTrack(track, stream);
    });
  }

  async function fetchData() {
    await getUserMedia();
    await makeConnection();
    await connect();
  }
  useEffect(() => {
    fetchData();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return (
    <StGameRoomOuter>
      <StGameRoomHeader>
        <Link to="/rooms">
          <button>뒤로가기</button>
        </Link>
        <Link to="/rooms">
          <button
            onClick={() => {
              leaveRoom();
            }}
          >
            방나가기
          </button>
        </Link>
        <button>설정</button>
      </StGameRoomHeader>
      <StGameRoomMain>
        <StGameTitleAndUserCards>
          <StTitle>
            <h1>주제</h1>
          </StTitle>
          <StUserCards>
            <StCard>
              Card
              <h4>키워드</h4>
              <span>OOO님</span>
              <div>
                {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
                <video
                  ref={videoRef}
                  id="myFace"
                  autoPlay
                  playsInline
                  width={200}
                  height={200}
                >
                  비디오
                </video>
                <button ref={muteBtn} onClick={onClickMuteHandler}>
                  mute
                </button>
                <button ref={cameraBtn} onClick={onClickCameraOffHandler}>
                  camera OFF
                </button>
                <select ref={camerasSelect} onInput={onInputCameraChange}>
                  <option>기본</option>
                  {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
                  <option ref={cameraOption} value="device" />
                </select>
              </div>
              <button>방장일 경우 시작버튼?</button>
            </StCard>
            <StCard>
              Card
              <h4>키워드</h4>
              <span>OOO님</span>
              <div>
                {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
                <video
                  ref={anotherVideoRef}
                  id="myFace"
                  autoPlay
                  playsInline
                  width={200}
                  height={200}
                >
                  비디오
                </video>
              </div>
            </StCard>
          </StUserCards>
        </StGameTitleAndUserCards>
        <StTimer>타이머:남은시간20초</StTimer>
        <StChatBox>
          <StNotice>공지내용</StNotice>
          <StUserChatBox>채팅내용</StUserChatBox>
          <StSendChat>
            <input placeholder="채팅내용" />
            <button>전송</button>
          </StSendChat>
        </StChatBox>
      </StGameRoomMain>
    </StGameRoomOuter>
  );
}

export default GameRoomAll;

const StGameRoomOuter = styled.div`
  border: 5px solid black;
  display: grid;

  grid-template-rows: 100px 1fr;
`;
const StGameRoomHeader = styled.div`
  border: 3px solid red;
`;

const StGameRoomMain = styled.div`
  margin-top: 30px;
  border: 3px solid blue;
  display: grid;
  grid-template-columns: 1fr 150px 1fr;
`;

const StGameTitleAndUserCards = styled.div`
  border: 2px solid black;
`;

const StTimer = styled.div`
  border: 2px solid black;
`;

const StChatBox = styled.div`
  border: 2px solid black;
  display: grid;
  grid-template-rows: 30px 1fr 30px;
`;

const StTitle = styled.div`
  border: 1px solid black;
  display: grid;
  grid-template-rows: 120px 1fr;
`;

const StUserCards = styled.div`
  border: 1px solid black;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
`;

const StCard = styled.div`
  border: 1px solid black;
`;

const StNotice = styled.div`
  border: 1px solid black;
`;

const StUserChatBox = styled.div`
  border: 1px solid black;
`;

const StSendChat = styled.div`
  border: 1px solid black;
`;

이번 코드에선 STUN 서버를이용하였는데 구글에서 제공하는 코드를 사용하였습니다.
실제 서비스에선 STUN서버를 구축하여 사용할 예정입니다.
아직 1:1 peer to peer 통신밖에 되지 않으며 채널이나 새로운 로직을 구현하여
최대 4명까지 통신 가능한 코드를 구현할 계획입니다.

profile
개발 블로그

0개의 댓글