React, Node, WebRTC (peerjs)와 영상 채팅 및 화면 공유
본문
화상 채팅 및 화면 공유 응용 프로그램을 만들려면 세 가지 주요 설정이 필요합니다.
https://dev.to/arjhun777/video-chatting-and-screen-sharing-with-react-node-webrtc-peerjs-18fg
- UI 처리를 위한 기본 React 설정.
- 소켓 연결을 유지하려면 백엔드 (Nodejs)가 필요합니다.
- 피어-투-피어 연결을 유지하고 유지하려면 피어 서버가 필요합니다.
1) 백엔드에 API를 호출하고 고유 ID를 얻고 사용자를 방에 참여하도록 리디렉션 하는 참여 버튼으로 반응 기본 설정 (포트 3000에서 실행되는 반응)
Frontend - ./Home.js
import Axios from 'axios';
import React from 'react';
function Home(props) {
const handleJoin = () => {
Axios.get(`http://localhost:5000/join`).then(res => {
props.history?.push(`/join/${res.data.link}?
quality=${quality}`);
})
}
return (
<React.Fragment>
<button onClick={handleJoin}>join</button>
</React.Fragment>
)
}
export default Home;
여기서 우리의 백엔드는 포트 localhost 5000에서 실행됩니다. 응답은 다음 단계에서 룸 ID로 사용될 고유 ID를 얻습니다.
2) 백엔드-서버가 포트 5000에서 수신 대기하고 "/ join"으로 라우터를 정의하여 고유 ID를 생성하고 이를 프런트 엔드로 반환하는 노드 기본 설정
Backend - ./server.js
import express from 'express';
import cors from 'cors';
import server from 'http';
import { v4 as uuidV4 } from 'uuid';
const app = express();
const serve = server.Server(app);
const port = process.env.PORT || 5000;
// Middlewares
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/join', (req, res) => {
res.send({ link: uuidV4() });
});
serve.listen(port, () => {
console.log(`Listening on the port ${port}`);
}).on('error', e => {
console.error(e);
});
여기에서 uuid 패키지를 사용하여 고유 한 문자열을 생성합니다.
3) 프런트 엔드에서 응답에 있는 ID로 새 경로를 만듭니다 (예 : "http : // localhost : 3000 / join / a7dc3a79-858b-420b-a9c3-55eec5cf199b"). 새로운 컴포넌트-RoomComponent가 연결 해제 버튼으로 생성되고 id = "room-container"가 포함 된 div 컨테이너가 비디오 요소를 보유합니다.
Frontend - ../RoomComponent.js
const RoomComponent = (props) => {
const handleDisconnect = () => {
socketInstance.current?.destoryConnection();
props.history.push('/');
}
return (
<React.Fragment>
<div id="room-container"></div>
<button onClick={handleDisconnect}>Disconnect</button>
</React.Fragment>
)
}
export default RoomComponent;
4) 이제 장치 캠과 마이크의 스트림이 필요합니다. 네비게이터를 사용하여 장치 스트림 데이터를 가져올 수 있습니다. 이를 위해 헬퍼 클래스 (Connection)를 사용하여 모든 수신 및 발신 스트림 데이터를 유지하고 백엔드와의 소켓 연결을 유지할 수 있습니다.
Frontend - ./connection.js
import openSocket from 'socket.io-client';
import Peer from 'peerjs';
const { websocket, peerjsEndpoint } = env_config;
const initializePeerConnection = () => {
return new Peer('', {
host: peerjsEndpoint, // need to provide peerjs server endpoint
// (something like localhost:9000)
secure: true
});
}
const initializeSocketConnection = () => {
return openSocket.connect(websocket, {// need to provide backend server endpoint
// (ws://localhost:5000) if ssl provided then
// (wss://localhost:5000)
secure: true,
reconnection: true,
rejectUnauthorized: false,
reconnectionAttempts: 10
});
}
class Connection {
videoContainer = {};
message = [];
settings;
streaming = false;
myPeer;
socket;
myID = '';
constructor(settings) {
this.settings = settings;
this.myPeer = initializePeerConnection();
this.socket = initializeSocketConnection();
this.initializeSocketEvents();
this.initializePeersEvents();
}
initializeSocketEvents = () => {
this.socket.on('connect', () => {
console.log('socket connected');
});
this.socket.on('user-disconnected', (userID) => {
console.log('user disconnected-- closing peers', userID);
peers[userID] && peers[userID].close();
this.removeVideo(userID);
});
this.socket.on('disconnect', () => {
console.log('socket disconnected --');
});
this.socket.on('error', (err) => {
console.log('socket error --', err);
});
}
initializePeersEvents = () => {
this.myPeer.on('open', (id) => {
this.myID = id;
const roomID = window.location.pathname.split('/')[2];
const userData = {
userID: id, roomID
}
console.log('peers established and joined room', userData);
this.socket.emit('join-room', userData);
this.setNavigatorToStream();
});
this.myPeer.on('error', (err) => {
console.log('peer connection error', err);
this.myPeer.reconnect();
})
}
setNavigatorToStream = () => {
this.getVideoAudioStream().then((stream) => {
if (stream) {
this.streaming = true;
this.createVideo({ id: this.myID, stream });
this.setPeersListeners(stream);
this.newUserConnection(stream);
}
})
}
getVideoAudioStream = (video=true, audio=true) => {
let quality = this.settings.params?.quality;
if (quality) quality = parseInt(quality);
const myNavigator = navigator.mediaDevices.getUserMedia ||
navigator.mediaDevices.webkitGetUserMedia ||
navigator.mediaDevices.mozGetUserMedia ||
navigator.mediaDevices.msGetUserMedia;
return myNavigator({
video: video ? {
frameRate: quality ? quality : 12,
noiseSuppression: true,
width: {min: 640, ideal: 1280, max: 1920},
height: {min: 480, ideal: 720, max: 1080}
} : false,
audio: audio,
});
}
createVideo = (createObj) => {
if (!this.videoContainer[createObj.id]) {
this.videoContainer[createObj.id] = {
...createObj,
};
const roomContainer = document.getElementById('room-container');
const videoContainer = document.createElement('div');
const video = document.createElement('video');
video.srcObject = this.videoContainer[createObj.id].stream;
video.id = createObj.id;
video.autoplay = true;
if (this.myID === createObj.id) video.muted = true;
videoContainer.appendChild(video)
roomContainer.append(videoContainer);
} else {
// @ts-ignore
document.getElementById(createObj.id)?.srcObject = createObj.stream;
}
}
setPeersListeners = (stream) => {
this.myPeer.on('call', (call) => {
call.answer(stream);
call.on('stream', (userVideoStream) => {console.log('user stream data',
userVideoStream)
this.createVideo({ id: call.metadata.id, stream: userVideoStream });
});
call.on('close', () => {
console.log('closing peers listeners', call.metadata.id);
this.removeVideo(call.metadata.id);
});
call.on('error', () => {
console.log('peer error ------');
this.removeVideo(call.metadata.id);
});
peers[call.metadata.id] = call;
});
}
newUserConnection = (stream) => {
this.socket.on('new-user-connect', (userData) => {
console.log('New User Connected', userData);
this.connectToNewUser(userData, stream);
});
}
connectToNewUser(userData, stream) {
const { userID } = userData;
const call = this.myPeer.call(userID, stream, { metadata: { id: this.myID }});
call.on('stream', (userVideoStream) => {
this.createVideo({ id: userID, stream: userVideoStream, userData });
});
call.on('close', () => {
console.log('closing new user', userID);
this.removeVideo(userID);
});
call.on('error', () => {
console.log('peer error ------')
this.removeVideo(userID);
})
peers[userID] = call;
}
removeVideo = (id) => {
delete this.videoContainer[id];
const video = document.getElementById(id);
if (video) video.remove();
}
destoryConnection = () => {
const myMediaTracks = this.videoContainer[this.myID]?.stream.getTracks();
myMediaTracks?.forEach((track:any) => {
track.stop();
})
socketInstance?.socket.disconnect();
this.myPeer.destroy();
}
}
export function createSocketConnectionInstance(settings={}) {
return socketInstance = new Connection(settings);
}
여기에서 모든 소켓 및 피어 연결을 유지하기 위해 Connection 클래스를 만들었습니다. 위의 모든 함수를 살펴볼 것이므로 걱정하지 마십시오.
- (사용할 비디오 프레임 보내기)와 같은 연결 클래스를 설정하기 위해 구성 요소에서 일부 데이터를 보내는 데 사용할 수 있는 설정 개체 (선택 사항)를 가져 오는 생성자가 있습니다.
- 생성자 내에서 initializeSocketEvents() 및 initializePeersEvents() 두 메서드를 호출합니다.
initializeSocketEvents()-백엔드와 소켓 연결을 시작합니다.
initializePeersEvents()-피어 서버와 피어 연결을 시작합니다. - 그런 다음 네비게이터에서 오디오 및 비디오 스트림을 가져 오는 getVideoAndAudio() 함수가있는 setNavigatorToStream()이 있습니다. 네비게이터에서 비디오 프레임 속도를 지정할 수 있습니다.
- 스트림을 사용할 수 있는 경우 .then (streamObj)에서 확인하고 이제 스트림 객체를 createVideo()로 우회하는 스트림을 표시하는 비디오 요소를 만들 수 있습니다.
- 이제 우리 자신의 스트림을 가져온 후 setPeersListeners() 함수에서 피어 이벤트를 수신 할 시간입니다. 여기서 다른 사용자로부터 들어오는 비디오 스트림을 수신하고 peer.answer (ourStream)에서 데이터를 스트리밍 합니다.
- 그리고 우리는 기존 룸에 연결하고 피어 오브젝트의 userID로 현재 피어 연결을 추적하는 경우 스트림을 보낼 newUserConnection()을 설정합니다.
- 마지막으로 사용자가 연결을 끊을 때 dom에서 비디오 요소를 제거하는 removeVideo가 있습니다.
5) 이제 백엔드는 소켓 연결을 수신해야 합니다. 소켓 연결을 쉽게 하기 위해 소켓 "socket.io"를 사용합니다.
Backend - ./server.js
import socketIO from 'socket.io';
io.on('connection', socket => {
console.log('socket established')
socket.on('join-room', (userData) => {
const { roomID, userID } = userData;
socket.join(roomID);
socket.to(roomID).broadcast.emit('new-user-connect', userData);
socket.on('disconnect', () => {
socket.to(roomID).broadcast.emit('user-disconnected', userID);
});
});
});
이제 우리는 룸에 참여하기 위해 백엔드에 소켓 연결을 추가했으며, 룸 ID와 사용자 ID를 포함하는 userData로 프런트 엔드에서 트리거됩니다. 사용자 ID는 피어 연결을 만들 때 사용할 수 있습니다.
그런 다음 소켓은 이제 roomID (프런트 엔드에서 응답으로 받은 고유 ID에서)로 방을 연결했으며 이제 방에있는 모든 사용자에게 메시지를 보낼 수 있습니다.
이제 socket.to (roomID) .broadcast.emit ( 'new-user-connect', userData); 이를 통해 우리를 제외한 모든 사용자에게 메시지를 보낼 수 있습니다. 그리고 이 'new-user-connect'는 프런트 엔드에서 수신되므로 회의실에 연결된 모든 사용자가 새 사용자 데이터를 수신하게 됩니다.
6) 이제 다음 명령을 사용하여 peerjs 서버를 만들어야 합니다.
npm i -g peerjs
peerjs --port 9000
7) 이제 Room Component에서 호출을 시작하기 위해 Connection 클래스를 호출해야 합니다. Room Component에서 이 기능을 추가합니다.
Frontend - ./RoomComponent.js
let socketInstance = useRef(null);
useEffect(() => {
startConnection();
}, []);
const startConnection = () => {
params = {quality: 12}
socketInstance.current = createSocketConnectionInstance({
params
});
}
이제 새로운 사용자가 참여할 때 방을 생성 한 후 사용자가 P2P 연결됨을 확인할 수 있습니다.
8) 이제 화면 공유를 위해 현재 스트림을 새 화면 공유 스트림으로 교체해야 합니다.
Frontend - ./connection.js
reInitializeStream = (video, audio, type='userMedia') => {
const media = type === 'userMedia' ? this.getVideoAudioStream(video, audio) :
navigator.mediaDevices.getDisplayMedia();
return new Promise((resolve) => {
media.then((stream) => {
if (type === 'displayMedia') {
this.toggleVideoTrack({audio, video});
}
this.createVideo({ id: this.myID, stream });
replaceStream(stream);
resolve(true);
});
});
}
toggleVideoTrack = (status) => {
const myVideo = this.getMyVideo();
if (myVideo && !status.video)
myVideo.srcObject?.getVideoTracks().forEach((track) => {
if (track.kind === 'video') {
!status.video && track.stop();
}
});
else if (myVideo) {
this.reInitializeStream(status.video, status.audio);
}
}
replaceStream = (mediaStream) => {
Object.values(peers).map((peer) => {
peer.peerConnection?.getSenders().map((sender) => {
if(sender.track.kind == "audio") {
if(mediaStream.getAudioTracks().length > 0){
sender.replaceTrack(mediaStream.getAudioTracks()[0]);
}
}
if(sender.track.kind == "video") {
if(mediaStream.getVideoTracks().length > 0){
sender.replaceTrack(mediaStream.getVideoTracks()[0]);
}
}
});
})
}
이제 현재 스트림은 reInitializeStream()이 교체해야 하는 유형을 확인해야 합니다. userMedia이면 캠 및 마이크에서 스트리밍 하고, 디스플레이 미디어 인 경우 getDisplayMedia()에서 디스플레이 스트림 객체를 가져온 다음 트랙을 전환하여 캠 또는 마이크를 중지하거나 시작합니다.
그런 다음 사용자 ID를 기반으로 새 스트림 비디오 요소가 생성되고 replaceStream()에 의해 새 스트림이 배치됩니다. 현재 호출 객체 저장소를 가져 오면 이전에 curretn 스트림 데이터가 replaceStream()의 새 스트림 데이터로 대체됩니다.
9) roomConnection에서 비디오와 화면 공유를 전환하는 버튼을 만들어야 합니다.
Frontend - ./RoomConnection.js
const [mediaType, setMediaType] = useState(false);
const toggleScreenShare = (displayStream ) => {
const { reInitializeStream, toggleVideoTrack } = socketInstance.current;
displayStream === 'displayMedia' && toggleVideoTrack({
video: false, audio: true
});
reInitializeStream(false, true, displayStream).then(() => {
setMediaType(!mediaType)
});
}
return (
<React.Fragment>
<div id="room-container"></div>
<button onClick={handleDisconnect}>Disconnect</button>
<button
onClick={() => reInitializeStream(mediaType ?
'userMedia' : 'displayMedia')}
>
{mediaType ? 'screen sharing' : 'stop sharing'}</button>
</React.Fragment>
)
그게 전부입니다. 화상 채팅 및 화면 공유 기능이 있는 응용 프로그램을 만듭니다.
내 작업 데모-Vichah