uniwrtc
v1.0.9
Published
A universal WebRTC signaling service
Maintainers
Readme
UniWRTC
A universal WebRTC signaling service that provides a simple and flexible HTTP polling signaling server for WebRTC applications.
Available on npm: https://www.npmjs.com/package/uniwrtc
Features
- 🚀 Simple signaling - HTTP polling (works locally and on Cloudflare Durable Objects)
- 🏠 Session-based architecture - Support for multiple sessions with isolated peer groups
- 🔌 Flexible client library - Ready-to-use JavaScript client for browser and Node.js
- 📡 Real-time messaging - Efficient message routing between peers
- 🔄 Auto-reconnection - Built-in reconnection logic for reliable connections
- 📊 Health monitoring - HTTP health check endpoint for monitoring
- 🎯 Minimal dependencies - Lightweight implementation with minimal runtime deps
Quick Start
Using with simple-peer (SDP text-only)
This repo's signaling format sends SDP as plain text for offers/answers.
simple-peer uses { type, sdp } objects, so use the adapter in simple-peer-adapter.js.
Example (browser):
import Peer from 'simple-peer';
import UniWRTCClient from './client-browser.js';
import { sendSimplePeerSignal, attachUniWRTCToSimplePeer, chooseDeterministicInitiator } from './simple-peer-adapter.js';
const client = new UniWRTCClient('https://your-signal-server', { roomId: 'my-room' });
await client.connect();
// Join a session (peers in the same session can connect)
await client.joinSession('my-room');
// Ensure exactly ONE side initiates for a given pair
const initiator = chooseDeterministicInitiator(client.clientId, targetId);
const peer = new Peer({ initiator, trickle: true });
const cleanup = attachUniWRTCToSimplePeer(client, peer);
peer.on('signal', (signal) => {
// targetId must be the other peer's UniWRTC client id
sendSimplePeerSignal(client, signal, targetId);
});
// When done:
// cleanup();Installation
From npm (recommended)
npm install uniwrtcRun the bundled server locally (installed binary is uniwrtc via npm scripts):
npx uniwrtc start # or: node server.js if using the cloned repoFrom source
git clone https://github.com/draeder/UniWRTC.git
cd UniWRTC
npm install
npm startThe signaling server will start on port 8080 by default.
Environment Configuration
Create a .env file based on .env.example:
cp .env.example .envConfigure the port:
PORT=8080Try the Demo
The interactive demo is available live at https://signal.peer.ooo/ (Cloudflare Workers deployment) or run locally:
Using the deployed demo (recommended):
- Open https://signal.peer.ooo/ in two browser tabs
- Default room is
demo-room—both tabs will auto-connect - Click "Connect" to join
- Watch the activity log to see peers connecting
- Open the P2P chat and send messages between tabs
Or run locally:
- Start the server:
npm start(signaling athttp://localhost:8080) - Start the Vite dev server:
npm run dev(demo athttp://localhost:5173/) - Open the demo in two browser tabs
- Enter the same session ID in both, then Connect
- Chat P2P once data channels open
Usage
Server API
The signaling server supports:
- HTTP polling signaling (no WebSockets)
Client → Server Messages
Join a session:
{
"type": "join",
"sessionId": "session-123"
}Leave a session:
{
"type": "leave",
"sessionId": "session-123"
}Send WebRTC offer:
{
"type": "offer",
"offer": "v=0\r\n...",
"targetId": "peer-client-id",
"sessionId": "session-123"
}Send WebRTC answer:
{
"type": "answer",
"answer": "v=0\r\n...",
"targetId": "peer-client-id",
"sessionId": "session-123"
}Send ICE candidate:
{
"type": "ice-candidate",
"candidate": "candidate:...|0|0",
"targetId": "peer-client-id",
"sessionId": "session-123"
}List available rooms:
{
"type": "list-rooms"
}Server → Client Messages
Welcome message (on connection):
{
"type": "welcome",
"clientId": "abc123",
"message": "Connected to UniWRTC signaling server"
}Session joined confirmation:
{
"type": "joined",
"sessionId": "session-123",
"clientId": "abc123",
"clients": ["xyz789", "def456"]
}Peer joined notification:
{
"type": "peer-joined",
"sessionId": "session-123",
"peerId": "new-peer-id"
}Peer left notification:
{
"type": "peer-left",
"sessionId": "session-123",
"peerId": "departed-peer-id"
}Client Library Usage
Use directly from npm:
// ESM (browser)
import UniWRTCClient from 'uniwrtc/client-browser.js';The client.js library provides a convenient wrapper for the signaling protocol:
// Create a client instance
const client = new UniWRTCClient('http://localhost:8080', { roomId: 'my-room' });
// Set up event handlers
client.on('connected', (data) => {
console.log('Connected with ID:', data.clientId);
});
client.on('joined', (data) => {
console.log('Joined session:', data.sessionId);
console.log('Existing peers:', data.clients);
});
client.on('peer-joined', (data) => {
console.log('New peer joined:', data.peerId);
// Initiate WebRTC connection with new peer
});
client.on('offer', (data) => {
console.log('Received offer from:', data.peerId);
// Handle WebRTC offer
});
client.on('answer', (data) => {
console.log('Received answer from:', data.peerId);
// Handle WebRTC answer
});
client.on('ice-candidate', (data) => {
console.log('Received ICE candidate from:', data.peerId);
// Add ICE candidate to peer connection
});
// Connect to the server
await client.connect();
// Join a session
await client.joinSession('my-session');
// Send WebRTC signaling messages
client.sendOffer(offerObject, targetPeerId);
client.sendAnswer(answerObject, targetPeerId);
client.sendIceCandidate(candidateObject, targetPeerId);Integration Example
Here's a complete example of creating a WebRTC peer connection:
const client = new UniWRTCClient('http://localhost:8080', { roomId: 'my-room' });
const peerConnections = new Map();
// ICE server configuration
const configuration = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
};
// Create peer connection
function createPeerConnection(peerId) {
const pc = new RTCPeerConnection(configuration);
pc.onicecandidate = (event) => {
if (event.candidate) {
client.sendIceCandidate(event.candidate, peerId);
}
};
pc.ontrack = (event) => {
// Handle incoming media stream
console.log('Received remote track');
};
peerConnections.set(peerId, pc);
return pc;
}
// Handle new peer
client.on('peer-joined', async (data) => {
const pc = createPeerConnection(data.peerId);
// Add local tracks
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// Create and send offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
client.sendOffer(offer, data.peerId);
});
// Handle incoming offer
client.on('offer', async (data) => {
const pc = createPeerConnection(data.peerId);
await pc.setRemoteDescription({ type: 'offer', sdp: data.offer });
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
client.sendAnswer(answer, data.peerId);
});
// Handle incoming answer
client.on('answer', async (data) => {
const pc = peerConnections.get(data.peerId);
if (pc) {
await pc.setRemoteDescription({ type: 'answer', sdp: data.answer });
}
});
// Handle ICE candidates
client.on('ice-candidate', async (data) => {
const pc = peerConnections.get(data.peerId);
if (pc) {
const [candidate, sdpMidRaw, sdpMLineIndexRaw] = String(data.candidate).split('|');
await pc.addIceCandidate(new RTCIceCandidate({
candidate,
sdpMid: sdpMidRaw || undefined,
sdpMLineIndex: sdpMLineIndexRaw !== undefined && sdpMLineIndexRaw !== '' ? Number(sdpMLineIndexRaw) : undefined
}));
}
});
// Connect and join session
await client.connect();
client.joinSession('my-video-session');
// Or use Cloudflare Durable Objects deployment (HTTP polling; no WebSockets)
const cfClient = new UniWRTCClient('https://signal.peer.ooo');
await cfClient.connect();
cfClient.joinSession('my-session');API Reference
UniWRTCClient
Constructor
new UniWRTCClient(serverUrl, options)Parameters:
serverUrl(string): HTTP(S) URL of the signaling serveroptions(object, optional):autoReconnect(boolean): Enable automatic reconnection (default: true)reconnectDelay(number): Delay between reconnection attempts in ms (default: 3000)
Methods
connect(): Connect to the signaling server (returns Promise)disconnect(): Disconnect from the serverjoinSession(sessionId): Join a specific session (peers isolated by session)leaveSession(): Leave the current sessionsendOffer(offer, targetId): Send a WebRTC offer to a specific peersendAnswer(answer, targetId): Send a WebRTC answer to a specific peersendIceCandidate(candidate, targetId): Send an ICE candidate to a specific peerlistRooms(): Request list of available sessions (legacy)on(event, handler): Register event handleroff(event, handler): Unregister event handler
Events
connected: Fired when connected to the serverdisconnected: Fired when disconnected from the serverjoined: Fired when successfully joined a roompeer-joined: Fired when another peer joins the roompeer-left: Fired when a peer leaves the roomoffer: Fired when receiving a WebRTC offeranswer: Fired when receiving a WebRTC answerice-candidate: Fired when receiving an ICE candidateroom-list: Fired when receiving the list of roomserror: Fired on error
Health Check
The server provides an HTTP health check endpoint for monitoring:
curl http://localhost:8080/healthResponse:
{
"status": "ok",
"connections": 5
}Architecture
Session-based Peer Isolation
- Sessions: Each session is identified by a unique string ID (also called "room" in the UI)
- Peer routing: Each peer gets a unique client ID; signaling messages are routed only to intended targets
- Session isolation: Peers in different sessions cannot see or communicate with each other
- Cloudflare Durable Objects: Uses DO state to isolate sessions; routing by
?room=query param per session - Clients join with
joinSession(sessionId)and receive notifications when other peers join the same session
Message Flow
- Client connects via HTTPS (Cloudflare DO HTTP polling)
- Server/Durable Object assigns a unique client ID
- Client sends join message with session ID
- Server broadcasts
peer-joinedto other peers in the same session only - Peers exchange WebRTC offers/answers/ICE candidates via the server
- Server routes signaling messages to specific peers by target ID (unicast, not broadcast)
Notes:
- Cloudflare signaling uses JSON over HTTPS requests to
/api(polling). - Offers/answers are transmitted as SDP strings (text-only) in the
offer/answerfields. - ICE candidates are transmitted as a compact text string:
candidate|sdpMid|sdpMLineIndex.
Security Considerations
This is a basic signaling server suitable for development and testing. For production use, consider:
- Adding authentication and authorization
- Implementing rate limiting
- Using TLS/HTTPS for encrypted connections
- Adding room access controls
- Implementing message validation
- Monitoring and logging
- Setting up CORS policies
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
