Видеочат и демонстрация экрана с помощью React, Node, WebRTC (peerjs)

  • 5 января, 15:37
  • 4142
  • 0

Для создания приложения для видеочата и демонстрации экрана требуется три основных настройки.

  1. Базовая настройка React для обработки пользовательского интерфейса.

  2.  Backend (Nodejs) для поддержки сокет-соединения.

  3. Одноранговый сервер для  создания однорангового соединения и его поддержки.

1) Базовая настройка с помощью кнопки присоединения, которая вызывает API-вызов бэкэнда, получает уникальный идентификатор и перенаправляет пользователя присоединиться к комнате (React работает на порту 3000)

Фронтенд - ./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, так как в ответ будет получен уникальный идентификатор, который будет использоваться в качестве идентификатора комнаты в следующих шагах.

2) Backend - базовая настройка узла с сервером, который прослушивает порт 5000 и определяет маршрутизатор с помощью "/ join" для создания уникального идентификатора и возврата его во внешний интерфейс.

Бэкэнд - ./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) В интерфейсе создается новый маршрут с идентификатором, полученным в ответе (выглядит примерно так: « http: // localhost: 3000 / join / a7dc3a79-858b-420b-a9c3-55eec5cf199b» ). Новый компонент - RoomComponent создается с помощью кнопки отключения и имеет контейнер div с id = "room-container" для хранения наших видеоэлементов.

Интерфейс - ../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) для поддержки всех входящих и исходящих потоковых данных и для поддержания соединения сокета с серверной частью.

Фронтенд - ./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. Затем у нас есть setNavigatorToStream () с функцией getVideoAndAudio (), которая будет получать аудио- и видеопоток от навигатора. Мы можем указать частоту кадров видео в навигаторе.
  4. Если поток доступен, мы будем разрешать в .then (streamObj), и теперь мы можем создать элемент видео для отображения нашего потока, минуя объект потока в createVideo ().
  5. Теперь, после получения нашего собственного потока, пришло время прослушивать одноранговые события в функции setPeersListeners (), где мы будем прослушивать любой входящий видеопоток от другого пользователя и передавать наши данные в peer.answer (ourStream).
  6. И мы будем устанавливать newUserConnection (), куда мы будем отправлять наш поток, если мы подключаемся к существующей комнате, а также отслеживаем текущее одноранговое соединение по userID в объекте peers.
  7. Наконец, у нас есть removeVideo, чтобы удалить элемент видео из dom, когда любой пользователь отключился.

5) Теперь серверная часть должна прослушивать соединение сокета. Использование socket "socket.io" для упрощения подключения к сокету.

Бэкэнд - ./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);        });    });
});


Теперь мы добавили сокетное соединение к бэкэнду для прослушивания присоединения к комнате, которое будет запускаться из внешнего интерфейса с userData, содержащим roomID и userID. ID пользователя доступен при создании однорангового соединения.

Затем сокет подключил комнату с roomID (из уникального идентификатора, полученного в качестве ответа во внешнем интерфейсе), и теперь мы можем отправлять сообщение всем пользователям в комнате.

Теперь socket.to (roomID) .broadcast.emit ('new-user-connect', userData); с этим мы можем отправить сообщение всем подключенным пользователям, кроме нас. И это соединение нового пользователя прослушивается во внешнем интерфейсе, поэтому все пользователи, подключенные к комнате, будут получать данные нового пользователя.

6) Вам нужно создать сервер peerjs, используя следующие команды

npm i -g peerjs
peerjs --port 9000


7) В компоненте Room нам нужно вызвать класс Connection, чтобы начать вызов. В Компонент Комнаты добавьте эту функциональность.

Фронтэнд - ./RoomComponent.js

    let socketInstance = useRef(null);        useEffect(() => {        startConnection();    }, []);    const startConnection = () => {        params = {quality: 12}        socketInstance.current = createSocketConnectionInstance({            params        });    }


Вы сможете увидеть, что после создания комнаты, когда новый пользователь присоединяется, пользователь будет одноранговым соединением.

8) Для совместного использования экрана вам нужно заменить текущий поток новым потоком для совместного использования экрана.

Фронтенд - ./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 (), а затем он переключит трек, чтобы остановить или запустить камеру или микрофон.

Затем новый элемент видео потока создается на основе идентификатора пользователя, а затем он помещает новый поток с помощью replaceStream (). Получив текущий объект вызова, хранилище ранее будет содержать данные текущего потока, которые будут заменены данными нового потока в replaceStream ().

9) В roomConnection нам нужно создать кнопку для переключения видео и демонстрации экрана.

Внешний интерфейс - ./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>
    )


Это все. Создайте приложение с видеочатом и демонстрацией экрана.


0 комментариев
Сортировка:
Добавить комментарий

IT Новости

Смотреть все