Logo

dev-resources.site

for different kinds of informations.

Building a Real-Time Video Chat App with WebRTC, Socket.io, Node.js, and React .

Published at
7/27/2024
Categories
webrtc
socket
react
videocall
Author
emilshiju
Categories
4 categories in total
webrtc
open
socket
open
react
open
videocall
open
Author
9 person written this
emilshiju
open
Building a Real-Time Video Chat App with WebRTC, Socket.io, Node.js, and React .

Full Implementation of Real-Time Video Chat

This document provides a complete implementation for a real-time video chat application using WebRTC and Socket.IO. The app enables seamless video and audio communication between users, with features for muting audio, pausing video, and handling call events.

Key Features:

Video and Audio Communication: Real-time streaming using WebRTC.
Call Management: Handle incoming and outgoing calls, including offer/answer and ICE candidates exchange.
User Controls: Toggle audio and video on/off, start and end calls.
Notifications: Alerts for call status and call management actions.
Implementation Details:

Socket.IO Integration: Manages real-time signaling and event handling.
RTCPeerConnection: Establishes peer-to-peer connections and handles ICE candidates.
Media Stream Handling: Manages local and remote media streams for video and audio.
UI Controls: Provides intuitive buttons for controlling video and audio.
Components:

App Component: The core component for initializing and managing video calls.
Event Handlers: Functions to manage offers, answers, candidates, and hangups.
User Interface: Responsive layout with controls for call actions.
This implementation ensures a robust and interactive user experience for real-time communication, with clear and effective UI elements for managing video calls.

Minimum APIs of WebRTC:
WebRTC consists of several key APIs that enable its functionality:

  • getUserMedia API: This API allows web applications to access the user’s camera and microphone, enabling real-time audio and video streaming.

  • RTCPeerConnection API: This API establishes and manages the peer-to-peer connection, facilitating audio and video communication between browsers. It handles network protocols, codecs, and security features like encryption.

  • RTCDataChannel API: In addition to audio and video streams, this API provides a peer-to-peer data channel for exchanging arbitrary data (files, text, etc.) directly between browsers.

To Know More

Image description



io.on("connection", (socket) => {
  console.log("Connected");

  socket.on("calling", (message) => {

    socket.broadcast.emit("calling", message);
  });

  socket.on("disconnect", () => {
    console.log("Disconnected");
  });
});



Enter fullscreen mode Exit fullscreen mode

To establish a direct connection between two specific users, you can use Socket.io to emit a calling event from one user directly to the intended recipient. This ensures that the connection request is sent specifically to the targeted user rather than broadcasting it to all connected users.



io.on("connection", (socket) => {
  console.log("Connected");


  socket.on("calling", (message) => {

    io.to(message.userId).emit("calling", message);

  });

  socket.on("disconnect", () => {
    console.log("Disconnected");
  });
});



Enter fullscreen mode Exit fullscreen mode

peer to peer connection



const configuration = {
  iceServers: [
    {
      urls: ["stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"],
    },
  ],
  iceCandidatePoolSize: 10,
};



try {
    pc.current = new RTCPeerConnection(configuration);
    pc.current.onicecandidate = (e) => {
      const message = {
        type: "candidate",
        candidate: null,
        id:userInfo,
      };
      if (e.candidate) {
        message.candidate = e.candidate.candidate;
        message.sdpMid = e.candidate.sdpMid;
        message.sdpMLineIndex = e.candidate.sdpMLineIndex;
      }
      socket.emit("calling", message);
    };
    pc.current.ontrack = (e) => (remoteVideo.current.srcObject = e.streams[0]);
    localStream.current.getTracks().forEach((track) => pc.current.addTrack(track, localStream.current));
    const offer = await pc.current.createOffer();
    socket.emit("calling", {id:userInfo, type: "offer", sdp: offer.sdp });
    await pc.current.setLocalDescription(offer);
  } catch (e) {
    console.log(e);
  }





Enter fullscreen mode Exit fullscreen mode

Handling Offers, Answers, and Candidates:



  try {
      pc.current = new RTCPeerConnection(configuration);
      pc.current.onicecandidate = (e) => {
        const message = {
          type: "candidate",
          id: userInfo,
          candidate: e.candidate ? e.candidate.candidate : null,
          sdpMid: e.candidate ? e.candidate.sdpMid : undefined,
          sdpMLineIndex: e.candidate ? e.candidate.sdpMLineIndex : undefined,
        };
        socket.emit("calling", message);
      };
      pc.current.ontrack = (e) => (remoteVideo.current.srcObject = e.streams[0]);
      localStream.current.getTracks().forEach((track) => pc.current.addTrack(track, localStream.current));
      await pc.current.setRemoteDescription(offer);
      const answer = await pc.current.createAnswer();
      socket.emit("calling", { id: userInfo, type: "answer", sdp: answer.sdp });
      await pc.current.setLocalDescription(answer);
    } catch (e) {
      console.log(e);
    }



Enter fullscreen mode Exit fullscreen mode

Handle Answer:



async function handleAnswer(answer) {
  if (!pc.current) {
    console.error("no peerconnection");
    return;
  }
  try {
    await pc.current.setRemoteDescription(answer);
  } catch (e) {
    console.log(e);
  }
}



Enter fullscreen mode Exit fullscreen mode

Handle Candidate:



 async function handleCandidate(candidate) {
    try {
      if (!pc.current) {
        console.error("no peerconnection");
        return;
      }
      await pc.current.addIceCandidate(candidate ? candidate : null);
    } catch (e) {
      console.log(e);
    }
  }



Enter fullscreen mode Exit fullscreen mode

Hangup Call:



async function hangup() {
  if (pc.current) {
    pc.current.close();
    pc.current = null;
  }
  localStream.current.getTracks().forEach((track) => track.stop());
  localStream.current = null;
  startButton.current.disabled = false;
  hangupButton.current.disabled = true;
  muteAudButton.current.disabled = true;
  muteVideo.current.disabled = true;

}



Enter fullscreen mode Exit fullscreen mode

React Component Implementation:





import { useRef, useEffect, useState } from "react";
import { FiVideo, FiVideoOff, FiMic, FiMicOff } from "react-icons/fi";
import { FaMicrophone, FaMicrophoneSlash, FaVideo, FaVideoSlash, FaPhone, FaTimes } from 'react-icons/fa';
import { io } from "socket.io-client";
import Swal from 'sweetalert2';

const socket = io("http://localhost:3000", { transports: ["websocket"] });

const configuration = {
  iceServers: [
    { urls: ["stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"] },
  ],
  iceCandidatePoolSize: 10,
};

function App() {

  const userInfo = 123456 // Assume userId is defined elsewhere

  const pc = useRef(null);
  const localStream = useRef(null);
  const startButton = useRef(null);
  const hangupButton = useRef(null);
  const muteAudButton = useRef(null);
  const localVideo = useRef(null);
  const remoteVideo = useRef(null);
  const muteVideoButton = useRef(null);
  const muteVideo = useRef(null);

  socket.on("calling", (e) => {
    if (!localStream.current) {
      console.log("not ready yet");
      return;
    }
    switch (e.type) {
      case "offer":
        handleOffer(e);
        break;
      case "answer":
        handleAnswer(e);
        break;
      case "candidate":
        handleCandidate(e);
        break;
      case "ready":
        if (pc.current) {
          alert("already in call ignoring");
          return;
        }
        makeCall();
        break;
      case "bye":
        if (pc.current) {
          hangup();
        }
        break;
      default:
        console.log("unhandled", e);
        break;
    }
  });



  async function makeCall() {
    try {
      pc.current = new RTCPeerConnection(configuration);
      pc.current.onicecandidate = (e) => {
        const message = {
          type: "candidate",
          candidate: e.candidate ? e.candidate.candidate : null,
          sdpMid: e.candidate ? e.candidate.sdpMid : undefined,
          sdpMLineIndex: e.candidate ? e.candidate.sdpMLineIndex : undefined,
          id: userInfo,
        };
        socket.emit("calling", message);
      };
      pc.current.ontrack = (e) => (remoteVideo.current.srcObject = e.streams[0]);
      localStream.current.getTracks().forEach((track) => pc.current.addTrack(track, localStream.current));
      const offer = await pc.current.createOffer();
      socket.emit("calling", { id: userInfo, type: "offer", sdp: offer.sdp });
      await pc.current.setLocalDescription(offer);
    } catch (e) {
      console.log(e);
    }
  }

  async function handleOffer(offer) {
    if (pc.current) {
      console.error("existing peerconnection");
      return;
    }
    try {
      pc.current = new RTCPeerConnection(configuration);
      pc.current.onicecandidate = (e) => {
        const message = {
          type: "candidate",
          id: userInfo,
          candidate: e.candidate ? e.candidate.candidate : null,
          sdpMid: e.candidate ? e.candidate.sdpMid : undefined,
          sdpMLineIndex: e.candidate ? e.candidate.sdpMLineIndex : undefined,
        };
        socket.emit("calling", message);
      };
      pc.current.ontrack = (e) => (remoteVideo.current.srcObject = e.streams[0]);
      localStream.current.getTracks().forEach((track) => pc.current.addTrack(track, localStream.current));
      await pc.current.setRemoteDescription(offer);
      const answer = await pc.current.createAnswer();
      socket.emit("calling", { id: userInfo, type: "answer", sdp: answer.sdp });
      await pc.current.setLocalDescription(answer);
    } catch (e) {
      console.log(e);
    }
  }

  async function handleAnswer(answer) {
    if (!pc.current) {
      console.error("no peerconnection");
      return;
    }
    try {
      await pc.current.setRemoteDescription(answer);
    } catch (e) {
      console.log(e);
    }
  }

  async function handleCandidate(candidate) {
    try {
      if (!pc.current) {
        console.error("no peerconnection");
        return;
      }
      await pc.current.addIceCandidate(candidate ? candidate : null);
    } catch (e) {
      console.log(e);
    }
  }

  async function hangup() {
    if (pc.current) {
      pc.current.close();
      pc.current = null;
    }
    localStream.current.getTracks().forEach((track) => track.stop());
    localStream.current = null;
    startButton.current.disabled = false;
    hangupButton.current.disabled = true;
    muteAudButton.current.disabled = true;
    muteVideo.current.disabled = true;

    closeVideoCall();
  }

  useEffect(() => {
    hangupButton.current.disabled = true;
    muteAudButton.current.disabled = true;
    muteVideo.current.disabled = true;
  }, []);

  const [audioState, setAudio] = useState(true);
  const [videoState, setVideoState] = useState(true);

  const startB = async () => {
    try {
      localStream.current = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: { echoCancellation: true },
      });
      localVideo.current.srcObject = localStream.current;
    } catch (err) {
      console.log(err);
    }

    startButton.current.disabled = true;
    hangupButton.current.disabled = false;
    muteAudButton.current.disabled = false;
    muteVideo.current.disabled = false;

    socket.emit("calling", { id: userInfo, type: "ready" });
  };

  const hangB = async () => {
    Swal.fire({
      title: 'Are you sure to cut the call?',
      showCancelButton: true,
      confirmButtonText: 'Yes',
      cancelButtonText: 'No',
    }).then((res) => {
      if (res.isConfirmed) {
        hangup();
        socket.emit("calling", { id: userInfo, type: "bye" });
      }
    });
  };

  function muteAudio() {
    if (localStream.current) {
      localStream.current.getAudioTracks().forEach(track => {
        track.enabled = !track.enabled; // Toggle mute/unmute
      });
      setAudio(!audioState); // Update state for UI toggle
    }
  }

  function pauseVideo() {
    if (localStream.current) {
      localStream.current.getVideoTracks().forEach(track => {
        track.enabled = !track.enabled; // Toggle video track
      });
      setVideoState(!videoState); // Update state for UI toggle
    }
  }

  return (
    <div className='bg-white w-screen h-screen fixed top-0 left-0 z-50 flex justify-center items-center'>
      <div className='flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4'>
        <div className='flex-1 p-4'>
          <div className='bg-gray-200 h-96 w-full md:w-96 rounded-lg shadow-md'>
            <video ref={localVideo} className='w-full h-full rounded-lg object-cover' autoPlay playsInline></video>
          </div>
        </div>
        <div className='flex-1 p-4'>
          <div className='bg-gray-200 h-96 w-full md:w-96 rounded-lg shadow-md'>
            <video ref={remoteVideo} className='w-full h-full rounded-lg object-cover' autoPlay playsInline></video>
          </div>
        </div>
      </div>
      <div className='absolute bottom-8 flex justify-center space-x-4'>
        <button className='p-2 rounded-full bg-gray-300 hover:bg-gray-400' ref={muteAudButton} onClick={muteAudio}>
          {audioState ? <FiMic /> : <FiMicOff />}
        </button>
        <button className='p-2 rounded-full bg-gray-300 hover:bg-gray-400' ref={startButton} onClick={startB}>
          <FaPhone className='text-gray-600' />
        </button>
        <button className='p-2 rounded-full bg-gray-300 hover:bg-gray-400' ref={muteVideo} onClick={pauseVideo}>
          {videoState ? <FaVideo className='text-gray-600' /> : <FaVideoSlash className='text-gray-600' />}
        </button>
        <button className='p-2 rounded-full bg-gray-300 hover:bg-gray-400' ref={hangupButton} onClick={hangB}>
          <FaTimes className='text-gray-600' />
        </button>
      </div>
    </div>
  );
}

export default App;







Enter fullscreen mode Exit fullscreen mode

Feel free to adjust the tone and content based on your audience and specific requirements.

webrtc Article's
30 articles in total
Favicon
Real-Time Voice Interactions over WebRTC
Favicon
6 Essential WebRTC Security Best Practices for 2025
Favicon
Build Voice AI Nextjs Apps with OpenAI Realtime API Beta (WebRTC) & shadcn/ui
Favicon
TCP Chatroom in Python
Favicon
Matrix-engine 2.x.x
Favicon
WebRTC WHIP & WHEP Tutorial: Build a live Streaming App
Favicon
The relationship between audio echo and distortion
Favicon
WebRTC python server: STUN/TURN servers for your python app
Favicon
Simple Peer Tutorial: Add TURN Server for Video, DataChannel
Favicon
Stun Protocol, Port and Traffic
Favicon
Galaxy A14G - Camera video feedback different than captured photo
Favicon
What is WebRTC and How Does It Work?
Favicon
TCP Vs UDP Protocol
Favicon
Janus WebRTC server and SFU: a real time video calling app
Favicon
Magic Three Project
Favicon
transcoding vs encoding
Favicon
Bandwidth Vs Throughput
Favicon
WebRTC Data Channels: A guide.
Favicon
WebRTC DataChannel Tutorial
Favicon
What is Jitter? Common Causes and how to reduce internet Jitter
Favicon
TypeScript WebRTC. How to implement a Deno Signaling Server 🌐GameLinkSafe
Favicon
An Exciting eLearning Experience with Capanicus Development Services
Favicon
Golang WebRTC. Como usar Pion 🌐Remote Controller
Favicon
SFU vs MCU vs P2P: WebRTC Architectures Explained
Favicon
Building a Real-Time Video Chat App with WebRTC, Socket.io, Node.js, and React .
Favicon
What is low Latency?
Favicon
WebRTC SFU: the complete Guide.
Favicon
How to use the CallKit and EnableX APIs to build iOS in-app calling
Favicon
Harnessing Analytics in Video Conferencing to Inform Product Strategy
Favicon
What is Simulcasting?

Featured ones: