[译]使用React, Node, WebRTC(peerjs)进行视频聊天和屏幕共享

1,236 阅读6分钟

原文链接:dev.to/arjhun777/v…

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

  1. 处理UI的基本React设置。

  2. 需要后端(No dejs)来维护套接字连接。

  3. 需要对等服务器来维护创建对等连接并维护它。

1)反应基本设置与加入按钮,使一个API调用后端,并获得一个唯一的id和重定向用户加入房间(反应运行在端口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,作为响应将获得一个唯一的id,该id将在即将到来的步骤中用作房间id。

2)后端-节点的基本设置,服务器在端口5000中监听,并用“/join”定义路由器,以生成唯一的id并将其返回前端

后端-./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=房间容器来保存我们的视频元素

前端-…/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);

}

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

  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”,让套接字连接变得轻松。

后端-./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类来启动调用。在房间组件中添加此功能。

前端-./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]);

                    }

                }

            });

        })

    }

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

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

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

前端-./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/