I have created a video chat app using WebRTC and MERN. However, I wanted to add a collaborative whiteboard to the app so that when a person draws on their canvas element the same shape will appear on the other user's canvas.
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 gets the other user's socket id and initializes the WebRTC connection using the functions provided by Context.js. It gets the remote user's video stream and references it to VideoPlayer.
Options.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 can display what you're drawing on the screen. However, it can't send the drawing over to the remote user. I've tried using WebSocket instead of WebRTC because I don't know how to do it with WebRTC. I think I have to add a reference to the canvas element in Whiteboard and Context.js would have to capture the stream from the canvas element.
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 serves as a signaling server to broker the WebRTC connection and relay the drawing events.
server.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});
});
});
How can I capture the drawings made by the remote user on my whiteboard?