Halloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PCHalloween DealsAmazon USOctober picks are moving - compare before checkoutAmazon US: current deals, useful picks and tech finds.See Picks×
FREEDOM251

Freedom251 › Blog › ICE

Build a Two-Browser Video Call in React with WebRTC

By Freedom251 Team · · 11 min read

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To build a two-person video call in React, combine three pieces: a RTCPeerConnection in each browser, a signaling channel that carries offers, answers, and ICE candidates, and media captured with getUserMedia(). WebRTC moves audio, video, and data between peers, but it does not discover the other browser or exchange negotiation messages for you.

Once the signaling path is in place, the call follows a predictable order: capture local media, add its tracks, create an offer, apply descriptions on the correct peer, exchange ICE candidates, render the remote track, and release every resource when the call or component ends. This guide shows that flow in React, including failure handling and cleanup.

As an Amazon Associate I earn from qualifying purchases.

What WebRTC handles—and what your React app must provide

MDN describes WebRTC as “a fully peer-to-peer technology for the real-time exchange of audio, video, and data, with one central caveat.” The caveat is signaling: the two browsers still need a way to discover one another and negotiate what they will send.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

WebRTC supplies browser APIs for media capture, session descriptions, ICE connectivity checks, and encrypted peer transport. Your application supplies the coordination around those APIs.

#1 Best Overall
Sale
Logitech C270 720p Webcam Plug-and-Play Wide Screen Video Calling - Black | Auto-Light Correct, Noise-Reducing Mic, Zoom, Microsoft, Teams, Skype, Nintendo Switch 2 New GameChat Mode, PC Mac compatible
  • Compatible with Nintendo Switch 2’s new GameChat mode
  • Crisp HD 720p/30 fps video calls with diagonal 55° field of view and auto light correction. Compatible with popular platforms including Skype and Zoom.
  • The built-in noise-reducing mic makes sure your voice comes across clearly up to 1.5 meters away, even if you’re in busy surroundings.
  • C270’s RightLight 2 feature adjusts to lighting conditions, producing brighter, contrasted images to help you look good in all your conference calls.
  • The adjustable universal clip lets you attach the camera securely to your screen or laptop, or fold the clip and set the webcam on a shelf. You’re always ready for your next video call.

The three parts of a basic call

  • Caller browser: captures a camera and microphone stream, creates an offer, and sends it through signaling.
  • Callee browser: receives the offer, applies it, creates an answer, and sends that answer back.
  • Signaling service: routes JSON messages to the intended peer. It can treat SDP as an opaque value; it does not need to understand or modify the offer and answer.
  • ICE servers: STUN and, when necessary, TURN servers help ICE discover a route between the browsers.

The media path in a successful call is negotiated by the peers. The signaling service is a control channel, not automatically a video relay. A production design may still use a relay for networking reasons, but that is a separate path from signaling.

Why peer-to-peer does not mean server-free

Without signaling, each browser has no standardized way to learn the other browser’s session description or candidate addresses. WebRTC does not prescribe WebSocket, HTTP, or any other signaling transport. Choose the transport that fits your application, then define message routing and room membership yourself.

Design the signaling messages first

A small message protocol keeps the React code understandable. Every message should identify the room or call and the intended recipient. The server should forward the payload without trying to rewrite SDP.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
{
  "type": "offer",
  "callId": "room-42",
  "description": {
    "type": "offer",
    "sdp": "..."
  }
}
{
  "type": "answer",
  "callId": "room-42",
  "description": {
    "type": "answer",
    "sdp": "..."
  }
}
{
  "type": "ice-candidate",
  "callId": "room-42",
  "candidate": {
    "candidate": "candidate:...",
    "sdpMid": "0",
    "sdpMLineIndex": 0
  }
}

The signaling server’s essential jobs are authentication or room authorization, routing each message to the other participant, and reporting disconnects. WebRTC itself does not define those application concerns.

WebSocket versus another transport

Choice Useful when What you must implement
WebSocket You want a long-lived, bidirectional channel for offers, answers, candidates, and call status. Connection authentication, room membership, message routing, reconnect behavior, and server-side cleanup.
HTTP or another application transport Your existing system already handles short-lived requests or queued messages. A way for both peers to deliver messages promptly, plus ordering, polling or push, and retry semantics.

Neither option changes the WebRTC offer/answer sequence. It only changes how your application delivers those messages.

Create the React video elements

Video elements are DOM objects, so use refs for imperative properties such as srcObject. Keep user-visible status in React state; keep the peer connection and media stream in refs so changing them does not cause a render on every event.

import { useEffect, useRef, useState } from "react";

export default function VideoCall({ signaling, callId, initiator }) {
  const localVideoRef = useRef(null);
  const remoteVideoRef = useRef(null);
  const peerRef = useRef(null);
  const localStreamRef = useRef(null);
  const pendingCandidatesRef = useRef([]);
  const [status, setStatus] = useState("idle");
  const [error, setError] = useState("");

  return (
    <section>
      <p role="status">{status}</p>
      {error && <p role="alert">{error}</p>}
      <div>
        <video ref={localVideoRef} autoPlay muted playsInline />
        <video ref={remoteVideoRef} autoPlay playsInline />
      </div>
    </section>
  );
}

muted on the local preview prevents the caller’s microphone from feeding back through the speakers. playsInline keeps playback inside the page on browsers that otherwise prefer a full-screen video mode.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Capture local media and create the peer connection

Call navigator.mediaDevices.getUserMedia({ video: true, audio: true }) when the call starts. The browser may prompt for permission, and the request can fail because the user denied access, no camera or microphone exists, or a device is unavailable. Treat acquisition as asynchronous and do not assume that a stream will arrive.

Rank #2
Sale
Logitech Brio 101 Full HD 1080p Webcam for Streaming and Meetings - Black | 1080p with auto-light correction, built-in mic, privacy shutter, USB-A, compatible with Teams, Google Meet, Zoom and more
  • Compatible with Nintendo Switch 2’s new GameChat mode
  • Auto-Light Balance: RightLight boosts brightness by up to 50%, reducing shadows so you look your best—compared to previous-generation Logitech webcams (1)
  • Privacy with a Slide: The integrated webcam cover makes it easy to get total, reliable privacy when you're not on a video call
  • Built-In Mic: The built-in microphone lets others hear you clearly during video calls
  • Easy Plug-And-Play: The Brio 101 works with most video calling platforms, including Microsoft Teams, Zoom and Google Meet—no hassle; it just works

After obtaining the stream, assign it to the local video element and add each track to the peer connection with addTrack(). Prefer addTrack(); the older addStream() API is obsolete.

const stream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
});

localStreamRef.current = stream;
if (localVideoRef.current) {
  localVideoRef.current.srcObject = stream;
}

for (const track of stream.getTracks()) {
  peer.addTrack(track, stream);
}

A USB webcam is optional. The same API works with a usable built-in camera, so external hardware is not a prerequisite for implementing the call.

Wire the events that make the call work

Render the incoming media with ontrack

The remote browser’s media arrives as a track event. Set the event’s stream on the remote video element. A browser can deliver more than one track, so preserve the stream supplied by the event rather than assuming a particular track order.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
peer.ontrack = (event) => {
  const [remoteStream] = event.streams;
  if (remoteVideoRef.current && remoteStream) {
    remoteVideoRef.current.srcObject = remoteStream;
  }
};

Forward candidates with onicecandidate

ICE gathers possible routes over time. Forward each non-null candidate through signaling as soon as it is produced. Candidate gathering can continue after media begins, so do not treat the first candidate as the complete set.

peer.onicecandidate = (event) => {
  if (event.candidate) {
    signaling.send(JSON.stringify({
      type: "ice-candidate",
      callId,
      candidate: event.candidate
    }));
  }
};

Expose connection state to the user

connectionState lets the interface distinguish connecting, connected, disconnected, and failed states. Use it for status text and recovery decisions; do not infer call health only from whether a video element exists.

peer.onconnectionstatechange = () => {
  switch (peer.connectionState) {
    case "connected":
      setStatus("connected");
      break;
    case "connecting":
      setStatus("connecting");
      break;
    case "disconnected":
      setStatus("disconnected");
      break;
    case "failed":
      setStatus("failed");
      break;
    case "closed":
      setStatus("closed");
      break;
    default:
      break;
  }
};

Follow the offer-and-answer sequence exactly

The order matters because each peer must apply the description received from the other side before generating its response.

  1. Caller creates a peer connection, registers handlers, captures local media, and adds tracks.
  2. Caller creates an offer with createOffer(), applies it with setLocalDescription(), and sends the resulting local description.
  3. Callee receives the offer and applies it with setRemoteDescription().
  4. Callee creates an answer with createAnswer(), applies it with setLocalDescription(), and sends the local answer.
  5. Caller receives the answer and applies it with setRemoteDescription().

In code, the caller’s initial negotiation looks like this:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const offer = await peer.createOffer();
await peer.setLocalDescription(offer);

signaling.send(JSON.stringify({
  type: "offer",
  callId,
  description: peer.localDescription
}));

The callee handles the offer like this:

await peer.setRemoteDescription(message.description);

const answer = await peer.createAnswer();
await peer.setLocalDescription(answer);

signaling.send(JSON.stringify({
  type: "answer",
  callId,
  description: peer.localDescription
}));

Finally, the caller applies the answer:

await peer.setRemoteDescription(message.description);

Do not create an answer before applying the remote offer. Doing so leaves the peers with incompatible signaling state.

Rank #3
Sale
EMEET C960 1080P Webcam with Microphone, 2 Mics, 90° FOV, Computer Camera | USB Type-A & A-to-C Adapter, Fixed Focus, Plug&Play for Zoom/Teams/Facetime/YouTube/Google Meet/Switch 2 Camera, 2025 Version
  • 1080P Webcam with Cover for Video Calls - EMEET computer webcam provides design and Optimization for professional video streaming. Realistic 1920 x 1080p video, 5-layer anti-glare lens, providing smooth video. C960 computer camera delivers 1920x1080 video with fixed focus (11.8–118.1 inches), so as to provide a clearer image. C960 USB webcam has a cover and can be removed automatically to meet your needs for privacy. For optimal image performance, use the webcam in a well-lit environment.
  • Built-in 2 Omnidirectional Mics - EMEET webcam with microphone for desktop features 2 built-in omnidirectional microphones, picking up your voice to create clear audio for communication. When installing the webcam, select EMEET C960 as the default microphone input device in your computer and video applications and select C960 as the default device in Zoom/Teams and ensure microphone permissions are enabled for proper use. Please note that C960 does not include built-in speakers.
  • Automatic Light Adjustment - Automatic exposure adjustment is applied in EMEET HD webcam 1080p so that the streaming webcam can deliver stable image performance. EMEET C960 camera for computer also features color adjustment and exposure optimization to help you look your best. For optimal video quality, it is recommended to use the webcam in normal or well-lit environments and select suitable video settings in your application. Proper lighting helps achieve a clearer and more balanced image.
  • Plug-and-Play & Upgraded USB Connectivity - New C960 webcam features both USB Type-A & A-to-C adapter connections for wider compatibility. For stable performance, connect the webcam directly to the computer's main USB port and ensure the device is recognized correctly. If a hub or docking station is used, please ensure it provides sufficient power and stable data transmission, as limited ports may affect performance. 90° wide-angle lens captures more participants without frequent adjustments.
  • High Compatibility & Multi Application - C960 webcam for laptop is compatible with Windows 10/11, macOS 10.14+, and Android TV 7.0+. Not supported: Windows Hello, TVs, tablets, or game consoles. It works with Zoom, Teams, Facetime, Google Meet, YouTube and more. Please select C960 webcam as the default camera and microphone device in your application and ensure camera/microphone permissions are enabled, especially on macOS. (Tips: Incompatible with Windows Hello)

Deliver ICE candidates at the right time

When a candidate message arrives, call addIceCandidate() on the matching peer connection. A candidate can arrive before the remote description has been applied, depending on signaling timing. Queue those early candidates and flush the queue immediately after setRemoteDescription() succeeds.

async function addCandidateOrQueue(peer, candidate) {
  if (peer.remoteDescription) {
    await peer.addIceCandidate(candidate);
  } else {
    pendingCandidatesRef.current.push(candidate);
  }
}

async function flushCandidates(peer) {
  const queued = pendingCandidatesRef.current.splice(0);
  for (const candidate of queued) {
    await peer.addIceCandidate(candidate);
  }
}

ICE tries possible connectivity routes. A direct candidate route is often preferable, but it is not guaranteed. Network address translation, firewalls, and restrictive enterprise policies can prevent a direct path.

STUN and TURN

Server type Role in ICE Trade-off
STUN Helps a browser learn the address that may be reachable through its network translation device. It assists discovery but does not relay media when peers cannot connect directly.
TURN Provides a relay path when direct routes fail. Improves reachability across restrictive networks, while adding relay bandwidth use, latency, and operating cost.

Configure authorized servers in the peer connection:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const peer = new RTCPeerConnection({
  iceServers: [
    { urls: "stun:your-authorized-stun.example" },
    {
      urls: "turn:your-authorized-turn.example",
      username: "temporary-user",
      credential: "temporary-credential"
    }
  ]
});

Use servers you own or are authorized to use. A public example endpoint is not automatically a production service, and a call should not promise a direct connection on every network.

Put the lifecycle in a React Effect

React Effects connect a component to external systems. A WebRTC peer connection, a media stream, a signaling socket, and video DOM nodes are all external objects. The Effect should create or join the call, and its cleanup should undo that setup.

Below is a compact component skeleton that includes the important ordering and teardown safeguards. Adapt the signaling event names to your server.

useEffect(() => {
  let active = true;
  let peer;

  const send = (message) => {
    signaling.send(JSON.stringify({ ...message, callId }));
  };

  const start = async () => {
    setStatus("connecting");
    setError("");

    peer = new RTCPeerConnection({
      iceServers: [
        { urls: "stun:your-authorized-stun.example" }
      ]
    });
    peerRef.current = peer;

    peer.ontrack = (event) => {
      const [stream] = event.streams;
      if (active && remoteVideoRef.current && stream) {
        remoteVideoRef.current.srcObject = stream;
      }
    };

    peer.onicecandidate = (event) => {
      if (event.candidate && active) {
        send({ type: "ice-candidate", candidate: event.candidate });
      }
    };

    peer.onconnectionstatechange = () => {
      if (!active) return;
      setStatus(peer.connectionState);
    };

    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: true
      });

      if (!active) {
        stream.getTracks().forEach((track) => track.stop());
        return;
      }

      localStreamRef.current = stream;
      if (localVideoRef.current) {
        localVideoRef.current.srcObject = stream;
      }
      stream.getTracks().forEach((track) => {
        peer.addTrack(track, stream);
      });

      if (initiator) {
        const offer = await peer.createOffer();
        if (!active) return;
        await peer.setLocalDescription(offer);
        send({ type: "offer", description: peer.localDescription });
      }
    } catch (reason) {
      if (!active) return;
      setStatus("failed");
      setError(formatMediaError(reason));
      closeCall();
    }
  };

  const onSignal = async (event) => {
    const message = JSON.parse(event.data);
    if (message.callId !== callId || !peerRef.current) return;

    try {
      if (message.type === "offer") {
        await peerRef.current.setRemoteDescription(message.description);
        await flushCandidates(peerRef.current);
        const answer = await peerRef.current.createAnswer();
        await peerRef.current.setLocalDescription(answer);
        send({ type: "answer", description: peerRef.current.localDescription });
      } else if (message.type === "answer") {
        await peerRef.current.setRemoteDescription(message.description);
        await flushCandidates(peerRef.current);
      } else if (message.type === "ice-candidate") {
        await addCandidateOrQueue(peerRef.current, message.candidate);
      }
    } catch (reason) {
      if (active) {
        setStatus("failed");
        setError("Signaling or ICE negotiation failed.");
      }
    }
  };

  signaling.addEventListener("message", onSignal);
  start();

  return () => {
    active = false;
    signaling.removeEventListener("message", onSignal);
    closeCall();
  };
}, [callId, initiator, signaling]);

The example assumes closeCall, flushCandidates, and addCandidateOrQueue are stable functions or are defined so the Effect dependency list remains correct. In a real component, include every reactive value used by the Effect in its dependencies.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why the active guard matters

Media acquisition can resolve after the user navigates away or the component unmounts. If that happens, stop the newly acquired tracks instead of attaching them to a closed peer connection. This prevents a late promise from reviving a call that React has already torn down.

Rank #4
Sale
Shcngqio 1080P HD Webcam with Microphone, Noise Cancellation, Auto Light Correction, Plug & Play USB Webcam for Laptop, Desktop, PC (Black-Gray)
  • 【1080P Full HD with Wide-Angle Lens】 Step up your video quality with the Shcngqio TWC29 1080p webcam. A wide-angle lens paired with a 1/2.9" CMOS sensor renders sharp, vibrant images and smooth 30fps footage—well suited for gaming, video calls, remote learning, live streaming, and everyday content creation. Fine details and natural colors come through clearly in every frame.
  • 【Built-In Noise-Canceling Microphone】 The TWC29 integrates an advanced noise-reducing microphone that picks up your voice while cutting down background ambient sound. Whether you're joining a webinar, hosting a conference, or streaming online, your speech stays crisp and understandable even in moderately noisy rooms.
  • 【Automatic Light Correction for Any Setting】 This webcam dynamically adjusts exposure and color temperature to match your surroundings. From low-light bedrooms to sunlit home offices, the auto light correction helps balance brightness and detail so you consistently present a clear, well-lit image during calls and recordings.
  • 【Slide-On Privacy Cover Included】 A dedicated privacy shield ships with every TWC29. Simply slide the cover over the lens when the camera is not in use to block the view and add a layer of everyday security. The slim, fitted design stays in place and does not interfere with image quality when open.
  • 【Plug-and-Play USB Setup, Works Across Devices】 No drivers, no complicated configuration. Connect the TWC29 via its 5ft USB cable to a USB 2.0, 3.0, or 3.1 port—and use OTG with supported mobile devices—and you're ready to go. The adjustable universal clip attaches firmly to laptops and desktop monitors, giving you a stable mounting point out of the box.

Expect an extra development cycle in Strict Mode

In development, React Strict Mode may run an additional setup and cleanup cycle. That is a test of whether setup and cleanup are symmetrical, not a second call that should be ignored. If the first cycle leaves a socket, camera track, or peer connection open, the second cycle exposes the leak.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Close every resource when the call ends

A call can end because the user clicks a hang-up button, the component unmounts, signaling closes, or the connection fails. Cleanup should be safe even when setup stopped halfway through.

function closeCall() {
  const peer = peerRef.current;
  if (peer) {
    peer.ontrack = null;
    peer.onicecandidate = null;
    peer.onconnectionstatechange = null;
    peer.close();
    peerRef.current = null;
  }

  const stream = localStreamRef.current;
  if (stream) {
    stream.getTracks().forEach((track) => track.stop());
    localStreamRef.current = null;
  }

  if (localVideoRef.current) {
    localVideoRef.current.srcObject = null;
  }
  if (remoteVideoRef.current) {
    remoteVideoRef.current.srcObject = null;
  }

  pendingCandidatesRef.current = [];
}

Only stop tracks your component owns. If a shared media manager supplies the stream, let that manager decide when the tracks should stop. Close or unsubscribe the signaling resource according to its ownership rules; otherwise a later message can target a peer connection that no longer exists.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshoot the failures users actually see

The browser refuses camera or microphone access

  • Show a clear message when the user denies permission; do not keep retrying automatically.
  • Handle missing devices and devices already in use. The media request can reject before a peer connection has any tracks.
  • If capture fails after a peer connection was created, close that partially started connection and stop any tracks acquired earlier.

Keep the permission error separate from signaling errors so the user knows whether to change a browser setting or retry the call.

The callee never receives an offer

  • Verify both browsers joined the same call or room identifier.
  • Log signaling message type and destination, but avoid logging sensitive SDP or credentials in production logs.
  • Confirm the caller sends the description after setLocalDescription(), not before.

addIceCandidate() fails

  • Check that the candidate is delivered to the intended peer.
  • Queue candidates that arrive before setRemoteDescription() and flush them afterward.
  • Ensure the candidate payload retains the fields supplied by the browser.

The state reaches failed or remains disconnected

  • Inspect whether the network blocks direct connectivity.
  • Configure an authorized TURN relay for restrictive networks; STUN alone cannot relay media.
  • Do not conclude that signaling is broken solely because a direct path was unavailable. ICE may need a relay candidate.

Audio echoes or video never appears

  • Keep the local preview muted.
  • Confirm that tracks were added before the offer was created.
  • Confirm the remote ontrack handler assigns the incoming stream to the remote video element.
  • Check that the component did not clean up immediately because an Effect dependency changed or Strict Mode exposed an incomplete lifecycle.

Production decisions to make before shipping

Authentication and room authorization

A signaling endpoint should verify who may join a call and which messages they may send. Never rely on a client-supplied room name as the only authorization check.

TURN operations

TURN relays consume bandwidth and need operational monitoring. Use credentials and servers you control or are permitted to use, and plan for relay capacity separately from the signaling service.

Reconnect and renegotiation

Connections can change after the initial call. Keep signaling available for additional ICE candidates and future negotiation rather than closing it as soon as the first video frame appears. If a peer is permanently closed, create a new peer connection instead of trying to reuse the closed object.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

State versus refs

Use state for labels such as “connecting,” “connected,” and “failed.” Use refs for RTCPeerConnection, MediaStream, pending candidates, and video nodes. This split prevents browser objects from becoming stale render values while still giving the user immediate status feedback.

A practical implementation checklist

  • Both peers create an RTCPeerConnection with the ICE configuration appropriate for your deployment.
  • The application captures media, assigns the local preview, and adds tracks with addTrack().
  • The caller creates and sets an offer before sending it.
  • The callee sets the remote offer before creating and setting an answer.
  • The caller sets the remote answer.
  • Each non-null ICE candidate is sent through signaling and passed to addIceCandidate() at the right time.
  • The remote stream is assigned in ontrack.
  • Permission, missing-device, signaling, and connection-state failures update visible status.
  • Unmounting or hanging up removes signaling listeners, closes the peer connection, stops owned media tracks, detaches video streams, and clears queued candidates.
  • Late asynchronous results are ignored or cleaned up after teardown.

With those pieces, React is responsible for the component lifecycle and user interface, while WebRTC handles the negotiated media transport. The signaling service connects the two state machines, and ICE determines whether the route is direct or must use a relay.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

ICEJavaScriptpeer-to-peerReactsignalingvideo chatWebRTC

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.