오늘은 RTC 시그널링을 드디어 완료 했습니다 ㅠㅠㅠ !!!!
N:N 연결 완료후 해당 방에서 나가는 로직까지 구현이 완료 되었습니다 !!!
아래의 코드는 금일까지 작성한 코드입니다
// 외부모듈
import styled from 'styled-components';
import React, { useRef, useEffect, useState, Children } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import * as SockJS from 'sockjs-client';
// 내부모듈
import { instance } from '../../../../api/core/axios';
import GameRoomChoice from './GameRoomChoice';
import { getNicknameCookie } from '../../../../utils/cookies';
import ChatBox from './ChatBox';
import Audio from './Audio';
function GameRoomRTC() {
const myNickName = getNicknameCookie('nickname');
console.log(myNickName);
const navigate = useNavigate();
const socketRef = useRef();
const videoRef = useRef(null);
const muteBtn = useRef(null);
const cameraBtn = useRef(null);
const camerasSelect = useRef(null);
const cameraOption = useRef(null);
const param = useParams();
const [users, setUsers] = useState([]);
let pcs = {};
let muted = false;
let cameraOff = false;
let stream;
let myPeerConnection;
function createPeerConnection(socketID, socket, peerConnectionLocalStream) {
const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302',
},
],
});
// add pc to peerConnections object
console.log(socketID);
console.log(socket);
console.log(peerConnectionLocalStream);
const keyName = socketID;
pcs = { ...pcs, [`${keyName}`]: pc };
console.log(pcs);
pc.onicecandidate = (e) => {
if (e.candidate) {
console.log('onicecandidate');
socket.send(
JSON.stringify({
type: 'candidate',
candidate: e.candidate,
receiver: socketID,
roomId: param.roomId,
}),
);
}
};
pc.oniceconnectionstatechange = (e) => {
// console.log(e);
};
// function handleAddStream(data) {
// console.log(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);
// }
// pc.onaddstream = (e) => {
// handleAddStream(e);
// };
pc.ontrack = (e) => {
console.log('ontrack success');
setUsers((oldUsers) => oldUsers.filter((user) => user.id !== socketID));
setUsers((oldUsers) => [
...oldUsers,
{
id: socketID,
stream: e.streams[0],
},
]);
};
if (peerConnectionLocalStream) {
console.log('localstream add');
peerConnectionLocalStream.getTracks().forEach((track) => {
pc.addTrack(track, peerConnectionLocalStream);
});
} else {
console.log('no local stream');
console.log(peerConnectionLocalStream);
}
return pc;
}
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() {
console.log('stream:', stream);
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);
}
return stream;
}
useEffect(() => {
socketRef.current = new SockJS(`http://13.209.84.31:8080/signal`);
socketRef.current.onopen = () => {
// navigator.mediaDevices
// .getUserMedia({
// video: true,
// audio: true,
// })
getUserMedia()
.then((stream) => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
console.log(stream);
}
// eslint-disable-next-line no-self-assign
stream = stream;
socketRef.current?.send(
JSON.stringify({
type: 'join_room',
roomId: param.roomId,
}),
);
})
.catch((error) => {
console.log(`getUserMedia error: ${error}`);
});
};
socketRef.current.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(data.type);
switch (data.type) {
case 'all_users': {
console.log('all_user recieve');
console.log(data.allUsers);
const { allUsers } = data;
for (let i = 0; i < allUsers.length; i += 1) {
console.log(stream);
createPeerConnection(allUsers[i], socketRef.current, stream);
console.log(pcs);
const allUsersEachPc = pcs[`${allUsers[i]}`];
if (allUsersEachPc) {
allUsersEachPc
.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
})
.then((offer) => {
console.log('create offer success');
allUsersEachPc.setLocalDescription(offer);
socketRef.current?.send(
JSON.stringify({
type: 'offer',
offer,
receiver: allUsers[i],
roomId: param.roomId,
}),
);
})
.catch((error) => {
console.log(error);
});
}
}
break;
}
case 'offer': {
console.log('get offer');
console.log(data);
console.log(data.offer);
createPeerConnection(data.sender, socketRef.current, stream);
const offerPc = pcs[`${data.sender}`];
if (offerPc) {
offerPc.setRemoteDescription(data.offer).then(() => {
console.log('answer set remote description success');
offerPc
.createAnswer({
offerToReceiveVideo: true,
offerToReceiveAudio: true,
})
.then((answer) => {
console.log('create answer success');
offerPc.setLocalDescription(answer);
socketRef.current?.send(
JSON.stringify({
type: 'answer',
answer,
receiver: data.sender,
roomId: param.roomId,
}),
);
})
.catch((error) => {
console.log(error);
});
});
}
break;
}
case 'answer': {
console.log('get answer');
console.log(pcs, data);
const answerPc = pcs[`${data.sender}`];
console.log(answerPc.signalingState);
if (answerPc) {
console.log(answerPc);
answerPc.setRemoteDescription(data.answer);
}
break;
}
case 'candidate': {
console.log('get candidate');
const candidatePc = pcs[`${data.sender}`];
console.log(candidatePc.signalingState);
if (candidatePc) {
candidatePc.addIceCandidate(data.candidate).then(() => {
console.log('candidate add success');
console.log(data.candidate, pcs);
});
}
break;
}
case 'leave': {
console.log('delete', data.sender);
pcs[`${data.sender}`].close();
delete pcs[data.sender];
setUsers((oldUsers) =>
oldUsers.filter((user) => user.id !== data.sender),
);
break;
}
default: {
break;
}
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const disconnect = () => {
socketRef.current?.send(
JSON.stringify({
type: 'leave',
roomId: param.roomId,
}),
);
};
const leaveRoom = async () => {
await disconnect();
await instance
.delete(`rooms/${param.roomId}/exit`)
.then(async (res) => {
console.log('res', res);
await navigate('/rooms');
})
.catch(async (error) => {
alert(error.data.message);
await navigate('/rooms');
});
};
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);
}
}
return (
<StGameRoomOuter>
<StGameRoomHeader>
<Link to="/rooms">
<button>뒤로가기</button>
</Link>
<button
onClick={() => {
leaveRoom();
}}
>
방나가기
</button>
<button>설정</button>
</StGameRoomHeader>
<GameRoomChoice props={param} />
<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>
</StCard>
{users.map((user) => {
return (
<StCard key={user.id}>
<Audio key={user.id} stream={user.stream}>
<track kind="captions" />
</Audio>
</StCard>
);
})}
</StUserCards>
</StGameTitleAndUserCards>
<StTimer>타이머:남은시간20초</StTimer>
<ChatBox />
</StGameRoomMain>
</StGameRoomOuter>
);
}
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;
`;
export default GameRoomRTC;
방나가기 기능까지 작동 잘 되며
현재 수정할 사항은 P2P 연결 이후 클라이언트에서
각자의 stream (audio ,vidoe) on,off 기능이 적용이 되지않는 부분 익일 수정 예정입니다.
이번 프로젝트에서 Web RTC 연결을 같은조 백엔드 소영님과
둘이서 거의 도맡아 하게 되었는데!!
소영님 알고보니 그림까지 잘 그리시는 능력자 : )
signailng 의 이해를 돕기위해 소영님이 그려주신 이미지를 아래에 올려 두겠습니당!
출처 소영님의 티스토리 !
https://littlezero48.tistory.com/260