What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
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.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →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
- 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.
{
"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.
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
- 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.
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.
- Caller creates a peer connection, registers handlers, captures local media, and adds tracks.
- Caller creates an offer with
createOffer(), applies it withsetLocalDescription(), and sends the resulting local description. - Callee receives the offer and applies it with
setRemoteDescription(). - Callee creates an answer with
createAnswer(), applies it withsetLocalDescription(), and sends the local answer. - Caller receives the answer and applies it with
setRemoteDescription().
In code, the caller’s initial negotiation looks like this:
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
- 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:
Recommended Free Tools
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.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteWhy 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
- 【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.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.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchTroubleshoot 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
ontrackhandler 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.
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
RTCPeerConnectionwith 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.
Quick Recap
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.

