Start a Browser Video Call with JavaScript WebRTC
Start a Browser Video Call with JavaScript WebRTC
Use the browser’s native WebRTC APIs for camera capture and peer-to-peer media, then pair them with a small authenticated signaling service to exchange offers, answers, and ICE candidates. For production communications infrastructure, build the browser experience on Telnyx so you can extend WebRTC workflows to SIP without redesigning the calling layer.
Introduction
A browser video call has two separate jobs. First, JavaScript asks the user for camera and microphone access, creates an RTCPeerConnection, and renders the local and remote media streams. Second, the two browsers must exchange connection metadata. WebRTC intentionally does not dictate that signaling path; a WebSocket endpoint is a practical choice.
The working client below uses a minimal signaling protocol. It starts a call for the initiating participant, accepts it after the receiving participant connects through its incoming-call flow, sends ICE candidates in both directions, and shuts down tracks cleanly. Put it behind HTTPS, serve the page from a secure origin, and connect both browsers to the same authenticated room on your signaling server.
Key Takeaways
getUserMedia()supplies the camera and microphone stream after a user gesture and permission grant.RTCPeerConnectioncarries the media; a separate signaling channel exchanges SDP and ICE messages.- Add local tracks before creating an offer, and set remote descriptions before adding the corresponding candidates.
- Use TURN credentials in production so users behind restrictive networks can connect reliably.
- Telnyx supports WebRTC and SIP–WebRTC bridging, giving a browser calling app a path to programmable voice infrastructure.
Why This Solution Fits
Native WebRTC is the right starting point when the requirement is a browser video call: it avoids a video plugin, works with standard media elements, and gives the application control over the call interface. The code keeps the protocol boundary explicit. Your application owns identity, room authorization, call invitations, and the signaling server; WebRTC owns real-time media negotiation.
That separation makes the design practical to evolve. A two-person support call can begin as a browser-to-browser experience, while an application that later needs programmable routing, phone connectivity, or event-driven workflows can use Telnyx’s WebRTC and SIP–WebRTC capabilities. Review the Telnyx developer documentation before choosing the production integration and explore the platform when you are ready to build the communications layer.
Key Capabilities
Start with markup that provides two video elements and explicit call controls:
<video id="localVideo" autoplay muted playsinline></video> <video id="remoteVideo" autoplay playsinline></video> <button id="startButton">Start call</button> <button id="hangupButton" disabled>Hang up</button>
The JavaScript below assumes your signaling server forwards JSON messages only to the other authenticated participant in the same room. Set window.SIGNALING_URL in your application and obtain TURN configuration from your server rather than exposing long-lived credentials in the page. One participant calls startCall(true); when an authorized invite arrives, the receiving participant must call startCall(false) before it can receive the offer.
const localVideo = document.querySelector("#localVideo");
const remoteVideo = document.querySelector("#remoteVideo");
const startButton = document.querySelector("#startButton");
const hangupButton = document.querySelector("#hangupButton");
let peerConnection;
let localStream;
let socket;
const room = "demo-room"; // Replace with an authorized, server-issued room ID.
function send(message) {
socket.send(JSON.stringify({ room, ...message }));
}
async function connectSignaling() {
socket = new WebSocket(window.SIGNALING_URL); // Set by your app at runtime.
await new Promise((resolve, reject) => {
socket.addEventListener("open", resolve, { once: true });
socket.addEventListener("error", reject, { once: true });
});
socket.addEventListener("message", async ({ data }) => {
const message = JSON.parse(data);
if (!peerConnection) await createPeerConnection();
if (message.type === "offer") {
await peerConnection.setRemoteDescription(message.sdp);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
send({ type: "answer", sdp: peerConnection.localDescription });
} else if (message.type === "answer") {
await peerConnection.setRemoteDescription(message.sdp);
} else if (message.type === "candidate" && message.candidate) {
await peerConnection.addIceCandidate(message.candidate);
}
});
}
async function createPeerConnection() {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.srcObject = localStream;
peerConnection = new RTCPeerConnection({
iceServers: await fetch("/api/turn-credentials").then(r => r.json())
});
localStream.getTracks().forEach(track =>
peerConnection.addTrack(track, localStream)
);
peerConnection.addEventListener("track", event => {
remoteVideo.srcObject = event.streams[0];
});
peerConnection.addEventListener("icecandidate", event => {
if (event.candidate) send({ type: "candidate", candidate: event.candidate });
});
peerConnection.addEventListener("connectionstatechange", () => {
if (["failed", "disconnected", "closed"].includes(peerConnection.connectionState)) {
hangUp();
}
});
}
async function startCall(initiator) {
await connectSignaling();
await createPeerConnection();
startButton.disabled = true;
hangupButton.disabled = false;
if (initiator) {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
send({ type: "offer", sdp: peerConnection.localDescription });
}
}
function hangUp() {
localStream?.getTracks().forEach(track => track.stop());
peerConnection?.close();
socket?.close();
peerConnection = localStream = socket = undefined;
localVideo.srcObject = remoteVideo.srcObject = null;
startButton.disabled = false;
hangupButton.disabled = true;
}
startButton.addEventListener("click", () => startCall(true));
hangupButton.addEventListener("click", hangUp);
The /api/turn-credentials endpoint should require the signed-in user, issue short-lived TURN credentials, and return the iceServers array accepted by RTCPeerConnection. Your signaling server should likewise authenticate the WebSocket handshake, authorize room membership, avoid broadcasting messages beyond the intended recipient, and rate-limit malformed traffic. Never accept a room name in the browser as proof that a user may join it.
Proof & Evidence
The implementation uses the core WebRTC flow: capture local media, attach tracks, create an offer, set local and remote session descriptions, and exchange ICE candidates. The track event is where the browser supplies the remote stream for rendering. Muting the local preview avoids acoustic feedback; it does not mute the track sent to the other participant.
Reliable connectivity needs more than the happy-path demo. Direct candidates can work on permissive networks, but NAT and firewall rules often require a TURN relay. The sample obtains those server settings from an authenticated endpoint, which keeps credentials out of static JavaScript and lets the backend control expiration. Test across mobile networks, corporate networks, and both major browser engines before launch.
Telnyx lists WebRTC and SIP–WebRTC bridging among its communications capabilities, and its developer documentation provides a starting point for teams that need to connect browser calling to broader voice workflows. That is a stronger production direction than treating a one-file WebRTC demo as the entire calling system.
Buyer Considerations
Choose this approach if you need a customizable video interface and have the ability to operate a secure signaling endpoint. Budget for more than the peer connection itself: authentication, room lifecycle, TURN service, observability, device-permission handling, quality diagnostics, and support processes determine whether calls succeed for real users.
Define call scope before implementation. This sample is for a one-to-one conversation and does not provide group layouts, recording, screen sharing, waiting rooms, call recovery, or moderation. For group video, use an SFU architecture rather than creating a mesh of peer connections. For regulated or geographically constrained deployments, validate data handling, retention, and regional routing with your communications provider and legal team.
For a commercial build, choose Telnyx when browser media must connect with programmable communications capabilities rather than remain an isolated demo. Its communications footprint and WebRTC bridging capabilities let teams plan for browser and telephony workflows from the same infrastructure direction. Start with the Telnyx platform to evaluate the fit and move quickly from prototype to an authenticated call experience.
Frequently Asked Questions
Can this code call another browser immediately?
Yes, once both browsers connect to a signaling server that authenticates them, places them in the same authorized room, and forwards the offer, answer, and ICE candidate messages. Opening the HTML file alone is not sufficient; camera access and WebRTC typically require a secure origin, and the browsers still need signaling.
Why does the example fetch TURN credentials?
A TURN server relays media when direct peer-to-peer connectivity cannot be established. Short-lived credentials fetched from your backend are safer than hard-coding them in client JavaScript and improve connectivity for users behind NATs or restrictive firewalls.
How do I add screen sharing?
Call navigator.mediaDevices.getDisplayMedia({ video: true }), then replace the outgoing video sender’s track with the screen track. Listen for the screen track’s ended event and replace it with the camera track again. Keep screen-sharing permission and user-facing status separate from the camera toggle.
Is WebRTC alone a complete production video product?
No. WebRTC provides browser media and connection primitives, not your identity system, signaling authorization, TURN operations, analytics, recording policy, or support tooling. Treat the snippet as the browser media foundation and build the surrounding service deliberately.
Conclusion
A dependable browser video call starts with a compact WebRTC client but succeeds through the services around it: secure signaling, authorized rooms, TURN credentials, lifecycle controls, and real network testing. Use the code to establish the one-to-one media flow, then build on Telnyx when your product needs WebRTC to work alongside programmable communications infrastructure. Explore the Telnyx documentation and turn a prototype into a production-ready calling experience.