[译]使用React/ Node/WebRTC(peerjs)创建视频聊天和屏幕共享软件

920 阅读7分钟

作者:Arjhun777。本文翻译自:Video Chatting and Screen Sharing with React, Node, WebRTC(peerjs)

创建视频聊天和屏幕共享应用程序需要三个主要设置

  1. 处理UI的基本React设置。
  2. 需要后端(No dejs)来维护套接字连接。
  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将在即将到来的步骤中用作房间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在响应中得到(看起来像这样"超文本传输协议://localhost:3000/join/a7dc3a79-858b-420b-a9c3-55eec5cf199b")。一个新的组件-Room Component创建与断开按钮和具有div容器id=房间容器来保存我们的视频元素

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);
}

在这里,我们创建了一个连接类来维护我们所有的套接字和对等连接,别担心,我们将浏览上面的所有函数。

  1. 我们有一个构造函数,它得到一个设置对象(可选),可以用来从我们的组件发送一些数据,用于设置我们的连接类,比如(发送视频帧)

  2. 在构造函数中,我们调用两个方法初始化Socket Events()和初始化Peers Events()

    • 初始化Socket Events()-将启动与我们后端的套接字连接。
  3. 初始化Peers Events()-将启动与我们的对等服务器的对等连接。

  4. 然后我们有set Navigator To Stream(),它有get Video And Audio()函数,它将从导航器获取音频和视频流。我们可以在导航器中指定视频帧率。

  5. 如果流是可用的,那么我们将在. than(stream Obj)中解析,现在我们可以创建一个视频元素来显示我们的流,绕过流对象来创建视频()。

  6. 现在,在获得我们自己的流之后,是时候在函数set Peers Listeners()中监听对等事件了,我们将监听来自另一个用户的任何传入视频流,并将在peer.answer(our Stream)中流式传输我们的数据。

  7. 我们将设置new User Connection(),如果我们连接到现有的房间,并通过对等对象中的userID跟踪当前对等连接,我们将在其中发送流。

  8. 最后,当任何用户断开连接时,我们可以从dom中删除视频元素。

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);
        });
    });
});

现在我们已经将套接字连接添加到后端以监听加入房间,这将从包含room ID和userID的user Data前端触发。用户ID在创建对等连接时可用。

然后套接字现在已经用room ID连接了一个房间(从前端的唯一id得到的响应),现在我们可以向房间里的所有用户发送消息。

现在socket.to(roomID).broadcast.emit('new-user-Connect', user Data);有了它,我们可以向除我们之外的所有用户的连接发送消息。这个“新用户连接”在前端被监听,所以房间里所有连接的用户都将接收新用户数据。

6)现在您需要使用以下命令创建一个peerjs服务器

npm i -g peerjs
peerjs --port 9000

7)现在在Room Component中,我们需要调用Connection类来启动调用。在房间组件中添加此功能。 Frontend - ./RoomComponent.js

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

现在,您将能够看到,在创建一个房间后,当一个新用户加入时,该用户将被点对点连接。

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]);
                    }
                }
            });
        })
    }

现在,当前流需要重新初始化Stream()将检查它需要替换的类型,如果它是user Media,那么它将从cam和micro流式传输,如果它的显示媒体,它将从get Display Media()获取显示流对象,然后它将切换轨道以停止或启动cam或micro。

然后,基于userID创建新的流视频元素,然后它将通过替换流()放置新流。通过获取当前调用对象存储库pre vio sly将包含的curr etn流数据将被替换为替换Stream()中的新流数据。

9)在room Connection,我们需要创建一个按钮来切换视频和屏幕共享。

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>
    )

这就是你所有的创建一个视频聊天和屏幕共享的应用程序。 祝你好运!!! 这是我的工作演示视频 看看我的博客-https://dev-ajs.blogspot.com/