Skip to main content

Optional Video chat Section!

Warning: this component is not a videoconference component, it is a video chat one to one person#

First of all, let's see the component and how to use it!#

The component appears when you click on the first icon (from the left) in the chat toolbox.

You have two features:

  • Invite someone!

  • Call someone!

    alt text

Click on "invite someone!

You have a button that appears, this button generates a connection id, click on it, the id is automatically copied in your clipboard.

alt text

Click in the conversation input and paste the previously copied id!

alt text

Once the message is sent, the user who receives the invitation to a button in the conversation bubble.

alt text

This button automatically copies the id in the clipboard...

alt text

And the video chat component appears automatically!

alt text

Click on "Call someone" and copy the id in the field.

alt text

When you click on the phone icon, the other user receives the call in real time.

alt text

Now let's see the code and logic behind it!#

Concerning the backend:#

The Server is in its simplest form so that you can, if you wish, join this part to the general backend of the chat.

require("dotenv").config();const express = require("express");const http = require("http");const app = express();const server = http.createServer(app);const io = require("socket.io")(server, {  cors: {    origin: process.env.FRONTEND_URL,    methods: ["GET", "POST"],  },});
io.on("connection", (socket) => {  socket.emit("me", socket.id);
  socket.on("disconnect", () => {    socket.broadcast.emit("callEnded");  });
  socket.on("callUser", (data) => {    io.to(data.userToCall).emit("callUser", {      signal: data.signalData,      from: data.from,      name: data.name,    });  });
  socket.on("answerCall", (data) => {    io.to(data.to).emit("callAccepted", data.signal);  });});
server.listen(process.env.SOCKET_VIDEO_CHAT_PORT, () =>  console.log(`server is running on port ${process.env.SOCKET_VIDEO_CHAT_PORT}`));

On the frontend side!#

you have a useVideoChat custom hook

// MODULES IMPORTSimport Peer from "simple-peer";import io from "socket.io-client";import { useEffect, useRef, useState } from "react";import { useRecoilState } from "recoil";// STATEMANAGMENT IMPORTSimport usernameAtom from "chatComponents/stateManager/atoms/usernameAtom";import callEndedAtom from "chatComponents/stateManager/atoms/callEndedAtom";import messagesAtom from "chatComponents/stateManager/atoms/messagesAtom";import roomIdAtom from "chatComponents/stateManager/atoms/roomIdAtom";
const socket = io.connect(process.env.REACT_APP_VIDEO_CHAT_WEBSERVICE);
const useVideoChat = () => {  const [me, setMe] = useState("");  const [stream, setStream] = useState();  const [receivingCall, setReceivingCall] = useState(false);  const [caller, setCaller] = useState("");  const [callerSignal, setCallerSignal] = useState();  const [callAccepted, setCallAccepted] = useState(false);  const [idToCall, setIdToCall] = useState("");  const [callEnded, setCallEnded] = useRecoilState(callEndedAtom);  const [name, setName] = useRecoilState(usernameAtom);  // eslint-disable-next-line no-unused-vars  const [messages, setMessages] = useRecoilState(messagesAtom);  const myVideo = useRef();  const userVideo = useRef();  const connectionRef = useRef();  const [roomName] = useRecoilState(roomIdAtom);
  useEffect(() => {    navigator.mediaDevices      .getUserMedia({ video: true, audio: true })      .then((stream) => {        setStream(stream);        myVideo.current.srcObject = stream;      });
    socket.on("me", (id) => {      setMe(id);      localStorage.setItem("me", id);      if (localStorage.getItem("username") !== null) {        let theName = localStorage.getItem("username");        setName(theName);      }    });
    socket.on("callUser", (data) => {      setReceivingCall(true);      setCaller(data.from);      setName(data.name);      setCallerSignal(data.signal);    });  }, [me, messages]);
  const callUser = (id) => {    const peer = new Peer({      initiator: true,      trickle: false,      stream: stream,    });    peer.on("signal", (data) => {      socket.emit("callUser", {        userToCall: id,        signalData: data,        from: me,        name: name,      });    });    peer.on("stream", (stream) => {      userVideo.current.srcObject = stream;    });    socket.on("callAccepted", (signal) => {      setCallAccepted(true);      peer.signal(signal);    });
    connectionRef.current = peer;  };
  const answerCall = () => {    setCallAccepted(true);    const peer = new Peer({      initiator: false,      trickle: false,      stream: stream,    });    peer.on("signal", (data) => {      socket.emit("answerCall", { signal: data, to: caller });    });    peer.on("stream", (stream) => {      userVideo.current.srcObject = stream;    });
    peer.signal(callerSignal);    connectionRef.current = peer;  };
  const leaveCall = () => {    setCallEnded(true);    setTimeout(() => {      window.location.replace(`/chat/${roomName}`);    }, 1200);    connectionRef.current.destroy();  };
  return {    stream,    myVideo,    callAccepted,    callEnded,    userVideo,    name,    setName,    me,    idToCall,    setIdToCall,    leaveCall,    callUser,    receivingCall,    answerCall,  };};
export default useVideoChat;

Then comes the component that uses the custom hook

// MODULES IMPORTSimport { Fragment, useEffect, useState } from "react";import { CopyToClipboard } from "react-copy-to-clipboard";import { useTranslation } from "react-i18next";import { useRecoilState } from "recoil";// CSS IMPORTSimport "./videoChatComponent.css";// HOOKS IMPORTSimport useVideoChat from "chatComponents/hooks/useVideoChat";// STATEMANAGMENT IMPORTSimport roomIdAtom from "chatComponents/stateManager/atoms/roomIdAtom";// COMPONENTS IMPORTSimport ChatRoom from "chatComponents/components/chatRoom/ChatRoom/ChatRoom";import Loader from "chatComponents/components/loader/Loader";// ASSETS IMPORTSimport CallIcon from "chatComponents/assets/callTo.svg";import CallFrom from "chatComponents/assets/callFrom.svg";import HangUpCall from "chatComponents/assets/hang-up.svg";import Assignement from "chatComponents/assets/assignment.svg";import AssignementWhite from "chatComponents/assets/assignment-white.svg";import Bell from "chatComponents/assets/sounds/mixkit-fairy-bells-583.mp3";import selectedDarkThemeAtom from "chatComponents/stateManager/atoms/selectedDarkThemeAtom";import { useAlert } from "react-alert";import useMobile from "chatComponents/hooks/useMobile";
const VideoChatComponent = () => {  const alert = useAlert();  const [roomName] = useRecoilState(roomIdAtom);  const [selectedDarkTheme] = useRecoilState(selectedDarkThemeAtom);  const { isMobile } = useMobile();
  const {    stream,    myVideo,    callAccepted,    callEnded,    userVideo,    name,    setName,    me,    idToCall,    setIdToCall,    leaveCall,    callUser,    receivingCall,    answerCall,  } = useVideoChat();  const [clickedInvitation, setClickedInvitation] = useState(false);  const [clickedCall, setClickedCall] = useState(false);  const { t } = useTranslation();
  const handleClickInvitation = () => {    if (clickedInvitation) {      setClickedInvitation(false);    }    if (!clickedInvitation) {      setClickedInvitation(true);      setClickedCall(false);    }  };  const handleClickCall = () => {    if (clickedCall) {      setClickedCall(false);    }    if (!clickedCall) {      setClickedCall(true);      setClickedInvitation(false);    }  };

  return (    <Fragment>      <ChatRoom />      {stream && (        <div          className={            isMobile              ? "video-chat-container video-chat-container-mobile"              : "video-chat-container"          }        >          <div            className={              callAccepted || receivingCall                ? selectedDarkTheme                  ? "myId marginTopContent dark-background white"                  : "myId marginTopContent light-background black"                : selectedDarkTheme                ? "myId dark-background white"                : "myId light-background black"            }            style={{ position: "relative" }}          >            <button              onClick={() => {                window.location.replace(`/chat/${roomName}`);              }}              className={                selectedDarkTheme                  ? "closed-video-chat-cross white"                  : "closed-video-chat-cross black"              }            >              X            </button>            {stream ? (              <div className="video-container">                {isMobile ? (                  <div className="video">                    {stream && (                      <video                        playsInline                        muted                        ref={myVideo}                        autoPlay                        style={{                          width: "240px",                          paddingLeft: 22,                          paddingRight: 22,                        }}                      />                    )}                  </div>                ) : (                  <div className="video">                    {stream && (                      <video                        playsInline                        muted                        ref={myVideo}                        autoPlay                        style={{                          width: "300px",                        }}                      />                    )}                  </div>                )}                <div className="video">                  {callAccepted && !callEnded ? (                    <video                      playsInline                      ref={userVideo}                      autoPlay                      style={{ width: "300px" }}                    />                  ) : null}                </div>              </div>            ) : (              <Loader />            )}            {!receivingCall && !callAccepted && (              <div className="inputs-video-chat">                <div className="you-section">                  <label htmlFor="Name">{t("youVideoCHat")}</label>                  <input                    disabled={name !== "" ? true : false}                    id="filled-basic1"                    label="Name"                    variant="filled"                    value={name}                    onChange={(e) => setName(e.target.value)}                    style={{ marginBottom: "20px" }}                  />                </div>                <hr style={{ width: "100%" }} />                <b                  style={{ cursor: "pointer" }}                  onClick={handleClickInvitation}                >                  {t("invitationVideoChat")}                </b>                <div                  className={                    clickedInvitation ? "invitation-section" : "hiddenParams"                  }                >                  <p style={{ width: "71%", textAlign: "left" }}>                    {t("copyId")}                  </p>                  <CopyToClipboard text={`${t("messageInvitation")}${me}`}>                    <button                      onClick={() =>                        alert.success(`${t("youHaveCopiedId")}: ${me}.`)                      }                      className={                        selectedDarkTheme                          ? "button-video-chat white"                          : "button-video-chat black"                      }                      variant="contained"                      color="primary"                    >                      <img                        src={selectedDarkTheme ? AssignementWhite : Assignement}                        alt="assigment"                        style={{ width: 30 }}                      />{" "}                      {t("clickToCopyId")}                    </button>                  </CopyToClipboard>                </div>                <hr style={{ width: "100%" }} />                <b style={{ cursor: "pointer" }} onClick={handleClickCall}>                  {t("callSomeOne")}                </b>                <div className={clickedCall ? "call-section" : "hiddenParams"}>                  <p style={{ width: "100%", textAlign: "center" }}>                    {t("pasteToCall")}                  </p>                  <div className="call-content">                    <input                      autoComplete="off"                      id="filled-basic"                      label="ID to call"                      variant="filled"                      value={idToCall}                      onChange={(e) => setIdToCall(e.target.value)}                    />                    <div className="call-button">                      {callAccepted && !callEnded ? (                        <button                          className="call-button-green"                          onClick={() => {                            leaveCall();                          }}                        >                          <img                            src={HangUpCall}                            style={{ width: 33 }}                            alt="endCall"                          />                        </button>                      ) : !receivingCall ? (                        <button                          className="call-button-green"                          aria-label="call"                          onClick={() => callUser(idToCall)}                        >                          <img                            src={CallIcon}                            style={{ width: 33 }}                            alt="call"                          />                        </button>                      ) : null}                      <p style={{ fontSize: 10 }}> {idToCall}</p>                    </div>                  </div>                </div>              </div>            )}            {callAccepted && !callEnded ? (              <button                className="call-button-green"                onClick={() => {                  leaveCall();                  window.location.replace(`/chat/${roomName}`);                }}              >                <img src={HangUpCall} style={{ width: 33 }} alt="endCall" />              </button>            ) : !receivingCall ? (              <button                className={clickedCall ? "hiddenParams" : "call-button-green"}                aria-label="call"                // onClick={() => callUser(idToCall)}                onClick={handleClickCall}              >                <img                  src={CallIcon}                  style={{ width: 33, marginTop: 22 }}                  alt="call"                />              </button>            ) : null}            {receivingCall && !callAccepted ? (              <div className="caller">                <audio loop autoPlay>                  <source src={Bell} />                </audio>                <h1                  className={selectedDarkTheme ? "white" : "black"}                  style={{ fontSize: 14 }}                >                  {name} {t("callMessage")}                </h1>                <button className="call-button-green" onClick={answerCall}>                  <img src={CallFrom} style={{ width: 33 }} alt="call" />                </button>              </div>            ) : null}          </div>        </div>      )}    </Fragment>  );};
export default VideoChatComponent;

And then, let see the tools chat in BottomChatComponent

...import { withVideoChat } from "postInstallConfig/withVideoChat";
const BottomChatComponent = (props) => {  const {    ...    VideoCall,    handleVideoChat,    ...  } = props;  return (    <div>      ...      {withVideoChat && (        <img          onClick={handleVideoChat}          style={{            width: 28,            marginRight: 15,            cursor: "pointer",            marginTop: -5,          }}          src={VideoCall}          alt="call"          className={plusSection ? "" : "hiddenParams"}        />      )}      ...    </div>  );};
export default BottomChatComponent;

And the video chat invitation section in MessagesComponent

...{message.body.includes("Invitation vidéo") ||  message.body.includes("Video invitation")    ? !message.ownedByCurrentUser &&      (!clickedCopyId ? (        <CopyToClipboard text={idChatInvitation}>          <button            disabled={clickedCopyId ? true : false}            className={              clickedCopyId                ? "idForCallInvitation-clicked"                : "idForCallInvitation"            }            onClick={() => {              setClickedCopyId(true);              alert.success(                `${t("youHaveCopiedId")}: ${idChatInvitation}.`              );            }}          >            Copiez          </button>        </CopyToClipboard>      ) : (        <Fragment>          <p>Le chat vidéo est entrain de démarrer...</p>        </Fragment>      ))    : null}...