분류 Nodejs

React, Node, WebRTC (peerjs)와 영상 채팅 및 화면 공유

컨텐츠 정보

  • 조회 22 (작성일 )

본문

화상 채팅 및 화면 공유 응용 프로그램을 만들려면 세 가지 주요 설정이 필요합니다.


https://dev.to/arjhun777/video-chatting-and-screen-sharing-with-react-node-webrtc-peerjs-18fg


  1. UI 처리를 위한 기본 React 설정.
  2. 소켓 연결을 유지하려면 백엔드 (Nodejs)가 필요합니다.
  3. 피어-투-피어 연결을 유지하고 유지하려면 피어 서버가 필요합니다.

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 클래스를 만들었습니다. 위의 모든 함수를 살펴볼 것이므로 걱정하지 마십시오.


  1. (사용할 비디오 프레임 보내기)와 같은 연결 클래스를 설정하기 위해 구성 요소에서 일부 데이터를 보내는 데 사용할 수 있는 설정 개체 (선택 사항)를 가져 오는 생성자가 있습니다.
  2. 생성자 내에서 initializeSocketEvents() 및 initializePeersEvents() 두 메서드를 호출합니다.
    initializeSocketEvents()-백엔드와 소켓 연결을 시작합니다.
    initializePeersEvents()-피어 서버와 피어 연결을 시작합니다.
  3. 그런 다음 네비게이터에서 오디오 및 비디오 스트림을 가져 오는 getVideoAndAudio() 함수가있는 setNavigatorToStream()이 있습니다. 네비게이터에서 비디오 프레임 속도를 지정할 수 있습니다.
  4. 스트림을 사용할 수 있는 경우 .then (streamObj)에서 확인하고 이제 스트림 객체를 createVideo()로 우회하는 스트림을 표시하는 비디오 요소를 만들 수 있습니다.
  5. 이제 우리 자신의 스트림을 가져온 후 setPeersListeners() 함수에서 피어 이벤트를 수신 할 시간입니다. 여기서 다른 사용자로부터 들어오는 비디오 스트림을 수신하고 peer.answer (ourStream)에서 데이터를 스트리밍 합니다.
  6. 그리고 우리는 기존 룸에 연결하고 피어 오브젝트의 userID로 현재 피어 연결을 추적하는 경우 스트림을 보낼 newUserConnection()을 설정합니다.
  7. 마지막으로 사용자가 연결을 끊을 때 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