Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Minimal WebRTC — a minimal, readable reference implementation

A deliberately small peer-to-peer video call app built to show how WebRTC signaling actually works: no SDK, no framework, no abstraction layers. Roughly 240 lines of JavaScript across a Socket.IO signaling server and a vanilla browser client.

If you have ever read the WebRTC spec and wanted to see the smallest possible thing that negotiates a call between two browsers, this is that thing.

Scope: one-to-one calls (two peers per room). See Limitations before using this as a starting point for anything real.

Demo


Why this exists

Most WebRTC tutorials either hide the negotiation behind a library or bury it in a production app. This repo keeps the interesting part visible:

  • Signaling is explicit. Every offer, answer, and ICE candidate crosses a Socket.IO channel you can read in ~60 lines of server.js.
  • No build step. Clone, npm install, npm start. The browser loads plain scripts.
  • The console is the documentation. Both server and client log each step of the handshake in order, so you can watch a connection form.

Quick start

git clone https://github.com/jeebon/minimal-webrtc.git
cd minimal-webrtc
npm install
npm start

Open http://localhost:3000 in two tabs, enter the same room number in both, and allow camera/microphone access.

New to WebRTC? Click Ping the other tab first. If the other tab alerts, the Socket.IO signaling channel works — so if the video call later fails, the problem is the WebRTC handshake, not your connection to the server.

Requires Node.js 14+.


How it works

sequenceDiagram
    participant A as Peer A (caller)
    participant S as Signaling server
    participant B as Peer B (callee)

    A->>S: create or join (room)
    S-->>A: created
    Note over A: getUserMedia() → local stream

    B->>S: create or join (room)
    S-->>B: joined
    Note over B: getUserMedia() → local stream
    B->>S: ready
    S-->>A: ready

    Note over A: createOffer() → setLocalDescription()
    A->>S: offer (SDP)
    S-->>B: offer (SDP)

    Note over B: setRemoteDescription() → createAnswer()
    B->>S: answer (SDP)
    S-->>A: answer (SDP)

    A->>S: candidate (ICE)
    S-->>B: candidate (ICE)
    B->>S: candidate (ICE)
    S-->>A: candidate (ICE)

    Note over A,B: Media flows directly, peer to peer
Loading

The signaling server never touches media. Once ICE negotiation succeeds, audio and video travel directly between browsers; the server's only job is introducing the two peers to each other.

Signaling events

Event Direction Purpose
create or join client → server Request a room by number
created / joined / full server → client Room assignment result
ready both Second peer has media and is ready to negotiate
offer / answer both SDP exchange
candidate both ICE candidate exchange

Room logic

server.js counts sockets in a room and responds accordingly:

  • 0 clientscreated (this peer becomes the caller)
  • 1 clientjoined (this peer becomes the callee, triggers ready)
  • 2+ clientsfull (rejected)

Project structure

server.js          Express static host + Socket.IO signaling (~60 lines)
public/
  index.html       Two <video> elements and a room-number input
  client.js        getUserMedia, RTCPeerConnection, and the handshake (~175 lines)

Limitations

These are intentional — removing them would defeat the point of the repo — but you should know about them before building on it.

Two peers only. This is a single RTCPeerConnection per client. Multi-party calls need either a full mesh (N−1 connections per peer, practical to about 4 participants) or an SFU such as mediasoup or LiveKit.

No TURN server. Only public STUN servers are configured, so connections succeed on the same network and across many home networks, but fail behind symmetric NAT or restrictive corporate firewalls. Add TURN credentials in the iceServers block of client.jscoturn is the standard self-hosted option.

HTTPS is required off localhost. getUserMedia() only works in a secure context. localhost is exempt, but testing across devices on your LAN needs TLS — a reverse proxy or ngrok is the quickest route.

No reconnection, no room cleanup, no auth. A dropped socket is a dropped call, and room numbers are guessable. Fine for a demo, not for production.

No error surfacing in the UI. Failures land in the browser console.


License

MIT — see LICENSE.

About

A simple webrtc application

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages