作者:Arjhun777。本文翻译自:Video Chatting and Screen Sharing with React, Node, WebRTC(peerjs)
创建视频聊天和屏幕共享应用程序需要三个主要设置
- 处理UI的基本React设置。
- 需要后端(No dejs)来维护套接字连接。
- 需要对等服务器来维护创建对等连接并维护它。
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);
}
在这里,我们创建了一个连接类来维护我们所有的套接字和对等连接,别担心,我们将浏览上面的所有函数。
-
我们有一个构造函数,它得到一个设置对象(可选),可以用来从我们的组件发送一些数据,用于设置我们的连接类,比如(发送视频帧)
-
在构造函数中,我们调用两个方法初始化Socket Events()和初始化Peers Events()
- 初始化Socket Events()-将启动与我们后端的套接字连接。
-
初始化Peers Events()-将启动与我们的对等服务器的对等连接。
-
然后我们有set Navigator To Stream(),它有get Video And Audio()函数,它将从导航器获取音频和视频流。我们可以在导航器中指定视频帧率。
-
如果流是可用的,那么我们将在. than(stream Obj)中解析,现在我们可以创建一个视频元素来显示我们的流,绕过流对象来创建视频()。
-
现在,在获得我们自己的流之后,是时候在函数set Peers Listeners()中监听对等事件了,我们将监听来自另一个用户的任何传入视频流,并将在peer.answer(our Stream)中流式传输我们的数据。
-
我们将设置new User Connection(),如果我们连接到现有的房间,并通过对等对象中的userID跟踪当前对等连接,我们将在其中发送流。
-
最后,当任何用户断开连接时,我们可以从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/