我使用 WebRTC 和 MERN 创建了一个视频聊天应用程序。但是,我想向应用程序添加协作白板,以便当一个人在其画布元素上绘图时,相同的形状将出现在其他用户的画布上。
Session.js
import React, { useState, useEffect, useRef } from "react";
import Videoplayer from "./Videoplayer";
import Options from "../pages/Options";
import { ContextProvider } from "../pages/Context";
import { Checklogin } from "./Checklogin";
function Session() {
return (
<div>
<Checklogin/>
<ContextProvider>
<Videoplayer/>
<Options/>
</ContextProvider>
</div>
);
}
export default Session;
Context.js
import React, {createContext, useState, useRef, useEffect} from 'react';
import {io} from 'socket.io-client';
import Peer from 'simple-peer';
const SocketContext = createContext();
var server = process.env.REACT_APP_SERVER;
const socket = io(`${server}`);
function ContextProvider({children}, props) {
const [stream, setStream] = useState(null);
const [me, setMe] = useState('');
const [call, setCall] = useState(null);
const [callEnded, setCallEnded] = useState(null);
const [callAccepted, setCallAccepted] = useState(null);
const [name, setName] = useState('');
const myVideo = useRef();
const userVideo = useRef();
const connectionRef = useRef();
var url = new URLSearchParams(window.location.search);
var id = url.get("id");
const canvas = useRef();
useEffect(() => {
navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then((currentStream) => {
setStream(currentStream);
if (myVideo.current)
myVideo.current.srcObject = currentStream;
});
socket.on('me', (id) => {setMe(id);});
socket.on('calluser', ({from, name: callerName, signal}) => {
setCall({isReceivedCall: true, from, name: callerName, signal});
});
}, []);
function callUser(id) {
var peer = new Peer({initiator: true, trickle: false, stream});
peer.on('signal', (data) => {
socket.emit('calluser', {userToCall: id, signalData: data, from: me, name})
});
peer.on('stream', (currentStream) => {
userVideo.current.srcObject = currentStream;
});
socket.on('callaccepted', (signal) => {
setCallAccepted(true);
peer.signal(signal);
});
connectionRef.current = peer;
}
function answerCall() {
if (call == null)
return;
setCallAccepted(true);
var peer = new Peer({initiator: false, trickle: false, stream});
peer.on('signal', (data) => {
socket.emit('answercall', {signal: data, to: call.from});
});
peer.on('stream', (currentStream) => {
userVideo.current.srcObject = currentStream;
});
peer.signal(call.signal);
connectionRef.current = peer;
}
function leaveCall() {
setCallEnded(true);
connectionRef.current.destroy();
window.location.reload();
}
return(
<SocketContext.Provider value={{ id, call, callAccepted, myVideo, userVideo, stream, name, setName, callEnded, me, callUser, leaveCall, answerCall}}>
{children}
</SocketContext.Provider>
)
};
export {ContextProvider, SocketContext};
Options.js 获取其他用户的套接字 id 并使用 Context.js 提供的函数初始化 WebRTC 连接。它获取远程用户的视频流并将其引用到 VideoPlayer。
选项.js
import { SocketContext } from "../pages/Context";
import React, { useContext, useEffect, useState, useRef } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import { ToastContainer, toast } from "react-toastify";
import Whiteboard from './Whiteboard';
function Options() {
const navigate = useNavigate();
const { me, call, leaveCall, callUser, answerCall, id } = useContext(SocketContext);
var flag = true;
var server = process.env.REACT_APP_SERVER;
const [peerSocket, setPeerSocket] = useState('');
useEffect(() => {
try {
axios.post(`${server}/session`,
{"me": me, "id": id},
{ withCredentials: true })
.then(res => {
if (!res.data.status) {
navigate('/');
}
});
}
catch (error) {
console.log(error);
}
}, [me, navigate]);
function getPeer() {
try {
axios.get(`${server}/session?id=${id}&me=${me}`,
{ withCredentials: true })
.then(res => {
if (res.data.status) {
setPeerSocket(res.data.msg);
}
});
}
catch (error) {
console.log(error);
}
};
var callPeer = function() {
getPeer();
if (peerSocket != '') {
callUser(peerSocket);
toast.success("Your partner will join you in a few moments", {position: "bottom-left"});
}
else {
getPeer();
callUser(peerSocket);
}
}
var endCall = function() {
flag = false;
leaveCall();
}
return (
<>
{call == null && flag? (<button id="callbtn" onClick={callPeer}>Call</button>):
(<button id="answerbtn" onClick={answerCall}>Answer</button>)}
<button id="hangupbtn" onClick={endCall}>Hang Up</button>
<Whiteboard id={id} me={me}/>
<ToastContainer/>
</>
)
}
export default Options;
Whiteboard.js 可以在屏幕上显示您正在绘制的内容。但是,它无法将绘图发送给远程用户。我尝试使用 WebSocket 而不是 WebRTC,因为我不知道如何使用 WebRTC。我想我必须在 Whiteboard 中添加对 canvas 元素的引用,并且 Context.js 必须从 canvas 元素捕获流。
Whiteboard.js
import React, {useContext, useState, useEffect, useRef } from "react";
import {io} from 'socket.io-client';
var server = process.env.REACT_APP_SERVER;
var socket = io(`${server}`);
function Whiteboard(props) {
const canvasRef = useRef(null);
const contextRef = useRef(null);
const [drawing, setDrawing] = useState(false);
var context, canvas;
useEffect(() => {
canvas = canvasRef.current;
canvas.width = window.innerWidth * 0.95;
canvas.height = window.innerHeight * 1.5;
canvas.style.width = `${canvas.width}px`;
canvas.style.height = `${canvas.height}px`;
context = canvas.getContext('2d');
context.scale(1, 1);
context.lineCap = 'butt';
context.strokeSytle = 'green';
context.lineWidth = 5;
contextRef.current = context;
}, []);
function startDrawing({nativeEvent}) {
var {offsetX, offsetY} = nativeEvent;
contextRef.current.beginPath();
contextRef.current.moveTo(offsetX, offsetY);
setDrawing(true);
}
function finishDrawing() {
contextRef.current.closePath();
setDrawing(false);
}
function draw({nativeEvent}) {
var {offsetX, offsetY} = nativeEvent;
if(!drawing) {
return
}
socket.on("draw", ({x, y}) => {
alert(1)
contextRef.current.lineTo(x, y);
contextRef.current.stroke();
});
if(props.peer != null && props.peer != '') {
var data = {
x: offsetX,
y: offsetY,
to: props.peer
};
socket.emit('draw', data); //emit coordinates of picture
}
contextRef.current.lineTo(offsetX, offsetY);
contextRef.current.stroke();
}
return (
<canvas
onMouseDown={startDrawing}
onMouseUp={finishDrawing}
onMouseMove={draw}
ref={canvasRef}>
</canvas>
);
}
export default Whiteboard;
Server.js 充当信号服务器来代理 WebRTC 连接并中继绘图事件。
服务器.js
io.on("connection", (socket) => {
socket.emit("me", socket.id);
socket.on("disconnect", () => {
socket.broadcast.emit("callended");
});
socket.on("calluser", ({ userToCall, signalData, from, name }) => {
io.to(userToCall).emit("calluser", { signal: signalData, from, name });
});
socket.on("answercall", (data) => {
io.to(data.to).emit("callaccepted", data.signal);
});
socket.on("draw", (data) => {
console.log(data)
io.to(data.to).emit("ondraw", {x: data.x, y: data.y});
});
});
如何捕获远程用户在我的白板上绘制的绘图?
您可以使用socket.io中的rooms来实现这一点。
这只是实现这一目标的一种方法。这可能适用于您的情况,也可能不适用,具体取决于有多少用户将与单个白板进行交互。您可能还需要限制某些用户编辑白板,您可以在服务器端使用优先级队列来确保一次更新 1 个像素。但只要后端逻辑是轻量级的,它就应该可以正常工作。