A lightweight library for creating peer-to-peer WebRTC conferencing with custom signaling drivers.
The library provides two small building blocks:
Sender
— creates outgoing PeerConnections, publishes local MediaStream and optional DataChannels, and sends offers to remote receivers through a signaling driver.Receiver
— listens for offers, answers them, and exposes remote streams and incoming data messages.
The library is signaling-agnostic: you must provide a driver with on(namespace, handler)
, off(namespace, handler)
and emit(namespace, data)
semantics. This allows usage with WebSocket, Pub/Sub services (such as NATS), server-side event buses, or a simple in-memory driver for prototypes.
Install the module:
npm install p2p
Run demo/watch:
npm run dev
Open http://localhost:8000/demo/ in two browser tabs to see it in action.
- Provide a signaling driver (see example below).
- Create and start a Sender if you want to broadcast a local MediaStream.
- Create and start a Receiver to discover and accept remote streams.
- Listen to events on Sender/Receiver to react to connections, errors, remote streams, and data messages.
This tiny driver is useful for local testing and examples. Replace it with your WebSocket or other signaling implementation in real apps.
// Minimal in-memory pub/sub driver
class MemoryDriver extends Map {
constructor() {
super();
}
on(namespace, handler) {
const k = namespace.join(':');
if (!this.has(k)) {
this.set(k, new Set());
}
this.get(k).add(handler);
}
off(namespace, handler) {
const k = namespace.join(':');
this.get(k)?.delete(handler);
}
emit(namespace, data) {
const k = namespace.join(':');
if (this.has(k)) {
for (const h of this.get(k)) {
try { h(data); } catch (e) { /* swallow */ }
}
}
}
}
Receiver listens for senders in a room and will respond to offers. It emits 'stream' when remote media arrives and 'message' for data channel messages.
import { Receiver } from 'p2p';
const driver = new MemoryDriver();
const receiver = new Receiver({ driver });
receiver.addEventListener('stream', (e) => {
const { id, stream } = e.detail;
console.log('stream received', id);
// attach to a video element
const video = document.createElement('video');
video.autoplay = true;
video.srcObject = stream;
document.body.appendChild(video);
});
receiver.addEventListener('channel', (e) => {
const { id } = e.detail;
console.log('data channel opened', id);
});
receiver.addEventListener('message', (e) => {
const { id, message } = e.detail;
console.log('message received', id, message);
});
receiver.addEventListener('dispose', (e) => {
const { id, error } = e.detail;
console.log('peer disposed', id, error);
});
// start listening in the same room as the sender
receiver.start({ room: 'demo-room' });
// stop when done
// receiver.stop();
Sender sends an offer to receivers in a room and publishes your local stream.
import { Sender } from 'p2p';
// create sender
const sender = new Sender({ driver });
sender.addEventListener('connect', (e) => {
const { id } = e.detail;
console.log('peer connected', id);
// send text data to the connected receiver
sender.send('Hello from sender!', id);
});
// prepare local stream (browser)
navigator.mediaDevices.getUserMedia({
audio: true,
video: true
}).then((stream) => {
// start sender, provide local stream and room name
sender.start({
stream,
room: 'demo-room',
dataChannel: true, // create data channels for each receiver
});
});
// to stop and close everything
// sender.stop();
A class or object that implements the pub/sub signaling protocol.
Methods
on(namespace: Array<string>, handler: (message: any) => void): void
— Subscribe a handler to the specified namespace; the handler will be called with the message payload when messages are emitted to that namespace.off(namespace: Array<string>, handler: (message: any) => void): void
— Unsubscribe a previously registered handler from the given namespace so it no longer receives messages.emit(namespace: Array<string>, message: any): void
— Publish a message to the specified namespace; all handlers subscribed to that namespace will be called with the message payload.
Signaling expectations
Namespaces used by the library:
- Sender listens on:
['sender', room]
and['sender', room, senderId]
- Sender emits to:
['receiver', room]
and['receiver', room, receiverId]
- Receiver listens on:
['receiver', room]
and['receiver', room, receiverId]
- Receiver emits to:
['sender', room]
and['sender', room, senderId]
Messages include type fields:
invoke
— request to connect (id only)offer
— sender -> receiver with SDP offer and metadataanswer
— receiver -> sender with SDP answercandidate
— ICE candidate exchangesync
— state sync (such as audio/video enabled)dispose
— tear down
A class creates outgoing PeerConnections, publishes local MediaStream and optional DataChannels, and sends offers to remote receivers through a signaling driver.
Constructor
driver
:object
— Signaling driver implementingon(namespace, handler)
,off(namespace, handler)
,emit(namespace, message)
.iceServers
:Array<RTCIceServer>
— RTCPeerConnectioniceServers
for NAT traversal.verify
:(id: string, credentials: any) => boolean
— Optional function to verify incoming connection requests; should returntrue
to accept orfalse
to reject.connectionTimeout
:number
— Time in seconds to wait for PeerConnection to connect (30 by default).queueSize
:number
— Maximum number of messages to queue if no channels are connected (10 by default).audioBitrate
:number
— Target audio bitrate (kbps).videoBitrate
:number
— Target video bitrate (kbps).audioCodecs
:Array<string>
— Preferred audio codec lists (in order, for example:['audio/opus']
).videoCodecs
:Array<string>
— Preferred video codec lists (in order, for example:['video/VP8', 'video/VP9']
).
Methods
start(options: object): void
— Begin broadcasting. Options:stream: MediaStream
— Local media to publish.room: string
— Room name to signal into (default
if omitted).state: object
— Arbitrary state sent with the offer.dataChannel: boolean
— Create per-peer data channels whentrue
(or when nostream
).
stop(): void
— Close all peer connections, data channels, and stop broadcasting.send(data: any, id?: string): void
— Senddata
over all open data channels to connected receivers.sync(state: object, merge?: boolean): void
— Update and sendstate
to all connected receivers.
Events
connect
:{ id: string, peer: RTCPeerConnection }
— Peer connection established.dispose
:{ id: string, peer: RTCPeerConnection, error?: Error }
— Peer connection closed.error
:{ id: string, error: Error }
— Non-fatal error occurred.
A class listens for offers, answers them, and exposes remote streams and incoming data messages.
Constructor
driver
:object
— Signaling driver withon/off/emit
.iceServers
:Array<RTCIceServer>
— Configuration of STUN or TURN servers.connectionTimeout
:number
— Time in seconds to wait for PeerConnection to connect (30 by default).pingInterval
:number
— Ping interval in seconds to re-establish connections (30 by default).pingAttempts
:number
— Number of ping attempts after all peers are gone. (10 by default).
Methods
start(options: object): void
— Begin listening for senders inroom
and respond to offers.room: string
— Room name to signal into (default
if omitted).credentials: any
— Optional credentials to identify/authorize the receiver.
stop(): void
— Close peers and stop listening.
Events
stream
:{ id: string, stream: MediaStream, state: object }
— Remote MediaStream received from peerid
.message
:{ id: string, message: any, state: object }
— Data channel message from peerid
.channel
:{ id: string, channel: RTCDataChannel, state: object }
— Data channel established with peerid
.sync
:{ id: string, state: object }
— Remote state changed (such as audio/video enabled).connect
:{ id: string, peer: RTCPeerConnection, state: object }
— Peer connection established.dispose
:{ id: string, peer: RTCPeerConnection, state: object, error?: Error }
— Peer connection closed.error
:{ id: string, error: Error }
— Non-fatal error occurred.
- For production, plug the driver to a server-backed signaling channel (WebSocket).
- Provide TURN servers in iceServers for NAT traversal in real-world deployments.
- The library sets preferred codecs and bitrate where supported — these are hints and may be ignored by browsers.
- Data channels are created per-peer and named by peer id.