Collaborative Whiteboard

Building a Collaborative Whiteboard

This guide will walk you through building a collaborative whiteboard using our React SDK and tldraw (opens in a new tab).

Demo

Before we get started, here's a demo of what we'll be building:

Walkthrough

Clone tldraw yjs example app and install dependencies

git clone https://github.com/Huddle01/collaborative-whiteboard
cd collaborative-whiteboard

Install dependencies

pnpm i  

Add .env file

cp .env.example .env

Add your own API_KEY and PROJECT_ID to the .env file. You can get these from here (opens in a new tab).

Run the app

pnpm dev  

Preparing Whiteboard

Tldraw (opens in a new tab) and yjs (opens in a new tab) is used to create this collaborative whiteboard. TLDraw provides easy to use library to create a whiteboard and other infinite canvas experiences. Yjs is used to sync the data of whiteboard among other peers. We took a reference from tldraw-yjs-example (opens in a new tab) to build real-time collaborative whiteboard.

Joining a Room

React SDK is used to enable audio/video functionality and share the data of cursor among other peers to move the viewport of user according to his/her cursor position. joinRoom from useRoom hook is used to join a room, but before that we have to create a room using Create Room API (opens in a new tab).

import { useRoom } from '@huddle01/react/hooks'
 
const { joinRoom } = useRoom({
    onJoin: () => {
      console.log('Joined room')
    },
})
 
const handleJoinRoom = async () => {
  await joinRoom({
    roomId: 'ROOM_ID',
    token: "YOUR_ACCESS_TOKEN"
  })
}

Handling Local Peer

useLocalPeer hook is used to update the metadata of local peer. Here, local peer refers the current user who is using the application. We will update the name of local peer using updateMetadata method from useLocalPeer hook.

LocalPeerData.tsx
import { useLocalPeer } from '@huddle01/react/hooks'
 
type PeerMetadata = {
  name: string
}
 
const { updateMetadata } = useLocalPeer<PeerMetadata>();
 
const handleUpdateMetadata = () => {
    updateMetadata({
        name: 'YOUR_NAME'
    })
}

useLocalVideo and useLocalAudio hooks are used to enable/disable the audio/video functionality of local peer.

LocalPeerData.tsx
import { useLocalVideo, useLocalAudio } from '@huddle01/react/hooks'
 
const { enableVideo, disableVideo, stream: videoStream, isVideoOn } = useLocalVideo()
const { enableAudio, disableAudio, isAudioOn } = useLocalAudio()
const videoRef = useRef<HTMLVideoElement>(null);
 
useEffect(() => {
    if (stream && videoRef.current) {
      videoRef.current.srcObject = stream;
    }
}, [stream]);
 
const handleVideo = () => {
    if (isVideoOn) {
      disableVideo();
    } else {
      enableVideo();
    }
}
 
const handleAudio = () => {
    if (isAudioOn) {
      disableAudio();
    } else {
      enableAudio();
    }
}
 
return (
    <div>
        <video ref={videoRef} autoPlay muted />
        <button onClick={handleVideo}>Video</button>
        <button onClick={handleAudio}>Audio</button>
    </div>
)

Handling Remote Peers

useRemotePeer hook is used to get the metadata of remote peers. Here, remote peers refers to the other users who are in the same room. This hook requires peerId which will be unique for each remote peer. You have to create a seperate hook to get the metadata of each remote peer. You can use usePeerIds hook to get list of peerIds of remote peers.

PeerData.tsx
import { useRemotePeer } from '@huddle01/react/hooks'
 
interface Props {
  peerId: string;
}
 
type PeerMetadata = {
  name: string
}
 
const PeerData = ({ peerId }: Props) => {
 
  // Get the metadata of remote peer
  const { metadata } = useRemotePeer<PeerMetadata>({ peerId });
  
  return (
    {/* Your UI */}
    {metadata.name}
  )
};

useRemoteVideo and useRemoteAudio hooks are used to enable/disable the audio/video functionality of remote peers.

PeerData.tsx
import { useRemotePeer } from '@huddle01/react/hooks'
import { useRemoteVideo, useRemoteAudio } from '@huddle01/react/hooks'
 
interface Props {
  peerId: string;
}
 
type PeerMetadata = {
  name: string
}
 
const PeerData = ({ peerId }: Props) => {
 
    // Get the metadata of remote peer
    const { metadata } = useRemotePeer<PeerMetadata>({ peerId });
 
    const { stream: videoStream, isVideoOn } = useRemoteVideo({ peerId });
    const { stream: audioStream, isAudioOn } = useRemoteAudio({ peerId });
 
    const videoRef = useRef<HTMLVideoElement>(null);
    const audioRef = useRef<HTMLAudioElement>(null);
 
    useEffect(() => {
        if (videoStream && videoRef.current) {
            videoRef.current.srcObject = videoStream;
        }
    }, [stream]);
 
    useEffect(() => {
        if (audioStream && videoRef.current) {
            audioRef.current.srcObject = audioStream;
        }
    }, [audioStream]);
 
    return (
        {/* Your UI */}
        <video ref={videoRef} autoPlay muted />
        <audio ref={audioRef} autoPlay>
    )
}

Sending Cursor Position Data

We will move the viewport of user according to his/her cursor position. To achieve this we will use sendData method from useDataMessage hook which will send the data of cursor position to remote peers.

import { useDataMessage } from '@huddle01/react/hooks'
import { useEffect, useState } from 'react'
 
const { sendData } = useDataMessage()
const [cursorPosition, setCursorPosition] = useState({
    top: 0,
    left: 0,
  });
 
// To track the cursor position we call onMouseMove method inside useEffect hook 
  useEffect(() => {
    const onMouseMove = (e: MouseEvent) => {
      const screenWidth = window.innerWidth;
      const screenHeight = window.innerHeight;
      const cursorWidth = 200; // adjust as needed
      const cursorHeight = 150; // adjust as needed
 
      // Adjust the cursor position to stay within the screen
      const adjustedTop = Math.min(e.clientY, screenHeight - cursorHeight);
      const adjustedLeft = Math.min(e.clientX, screenWidth - cursorWidth);
      
      setCursorPosition({
        top: adjustedTop + 15,
        left: adjustedLeft + 15,
      });
 
      // Send the cursor position data to remote peers
      sendData({
        to: '*',
        payload: JSON.stringify({
          top: adjustedTop + 15,
          left: adjustedLeft + 15,
        }),
        label: 'cursor',
      });
    };
    document.addEventListener('mousemove', onMouseMove);
    return () => {
      document.removeEventListener('mousemove', onMouseMove);
    };
  }, []);

Receiving Cursor Position Data

To receive the data of cursor position from remote peers we will use onMessage prop from useDataMessage hook.

import { useDataMessage } from '@huddle01/react/hooks'
import { useState } from 'react'
 
// State to store the cursor position data
const [cursorPosition, setCursorPosition] = useState({
    top: 0,
    left: 0,
});
 
// Receive the cursor position data from remote peers
useDataMessage({
    onMessage(payload, from, label) {
      if (label === 'cursor' && from === peerId) {
        const { top, left } = JSON.parse(payload);
        setCursorPosition({
          top: top,
          left: left,
        });
      }
    },
  });
 
return (
    <div
      style={{
        position: 'absolute',
        ...cursorPosition,
      }}
    >
    {/* Code to show viewport of remote peer which will move with cursor */}
    </div>
)

You're all set! Happy Hacking! 🎉

For more information, please refer to the React SDK Reference.

Audio/Video Infrastructure designed for the developers to empower them ship simple yet powerful Audio/Video Apps.
support
company
Copyright © 2024 Graphene 01, Inc. All Rights Reserved.