0

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?

1 Answers1

1

You can use rooms in socket.io to achieve this.

  1. Assign an ID to the whiteboard and emit an event "join-room" to the server as soon as all the members in the call have access to the created whiteboard.
  2. Now you can listen for events to the room and send emits to the client to each member in the call using a single socket emit.

Again this is just one way to achieve this. This might work for your case or it might not depending on how many users are going to interact with the single whiteboard. You might also need to restrict some users to edit the whiteboard you can use a priority queue on the server side to make sure 1 pixel at a time is being updated. But as long as the backend logic for this is lightweight it should work without any issues.