Simple WebRTC wrapper for peer to peer connection setup in browsers.
It creates portable offer and contract objects that you move through your own signaling channel.
RTCPeerConnection, RTCDataChannel,
MediaStream, EventTarget, CustomEvent, crypto.randomUUID; media
sharing also needs navigator.mediaDevices and document.npm install @sovereignbase/peer2peer
# or
pnpm add @sovereignbase/peer2peer
# or
yarn add @sovereignbase/peer2peer
# or
bun add @sovereignbase/peer2peer
# or
deno add jsr:@sovereignbase/peer2peer
# or
vlt install jsr:@sovereignbase/peer2peer
import { P2PConnection } from '@sovereignbase/peer2peer'
type Message = {
type: 'chat'
text: string
}
/**
* Package uses 4 Google STUN Servers by default,
* you can add additional turn or stun servers.
*
* For example `https://developers.cloudflare.com/realtime/turn/`.
*
* WebRTC is end-to-end encrypted by default.
* As long as you stick to standard STUN or TURN servers and
* do not use something like a SFU, all data is e2ee via DTLS
*/
// Peer A
const offer = await P2PConnection.makeOffer()
// send `offer` to peer B using your own transport
// Peer B
const copies = await P2PConnection.acceptOffer(offer)
// send `copies.offeror` back to peer A
// keep `copies.offeree` on peer B
const peerA = new P2PConnection<Message>(copies.offeror)
const peerB = new P2PConnection<Message>(copies.offeree)
peerA.addEventListener('message', (event) => {
console.log('peer A received', event.detail)
})
peerB.addEventListener('message', (event) => {
console.log('peer B received', event.detail)
})
await peerA.ready()
await peerB.ready()
peerA.sendMessage({
type: 'chat',
text: 'hello from peer A',
})
connection.addEventListener('camera', (event) => {
document.body.append(event.detail) // HTMLVideoElement
})
connection.addEventListener('screen', (event) => {
document.body.append(event.detail) // HTMLVideoElement
})
// Stream media
await connection.shareMicrophone()
await connection.shareCamera()
await connection.shareScreen()
// Stop streaming media
connection.stopSharingMicrophone()
connection.stopSharingCamera()
connection.stopSharingScreen()
Local preview elements are exposed as:
P2PConnection.localCameraVideoElementP2PConnection.localScreenVideoElementOffer and Contract
objects through your own channel, for example WebSocket, HTTP, QR, copy-paste
or any other out-of-band transport.P2PConnection.makeOffer() and P2PConnection.acceptOffer() always include 4
public Google STUN servers by default and append any additional ICE servers
you pass in.ready() resolves only after the underlying RTCDataChannel reaches the
"open" state. If the peer connection fails, closes, or the channel errors
first, it rejects with a typed P2PConnectionError.sendMessage() uses MessagePack encoding via @msgpack/msgpack. The
"message" event receives the decoded payload as event.detail.shareMicrophone() and shareCamera()
reuse one cached getUserMedia() stream, and shareScreen() reuses one
cached getDisplayMedia() stream.P2PConnection.localCameraVideoElement and
P2PConnection.localScreenVideoElement properties.Failures throw P2PConnectionError. The code is stable and the message
describes the specific failure site.
Supported error codes:
CHANNEL_ERRORCHANNEL_CLOSEDCHANNEL_NOT_AVAILABLECONNECTION_NOT_READYUNKNOWN_PEER_CONTRACTMISSING_LOCAL_DESCRIPTIONnpm test
100% for statements, branches, functions and
lines on the published runtime bundle.npm run bench
Last measured on Node v22.14.0 (win32 x64):
| Benchmark | Average | Min | Max |
|---|---|---|---|
websocket-signaled ready() |
242.39 ms | 229.80 ms | 284.30 ms |
one-way sendMessage() delivery |
5.36 ms | 3.21 ms | 8.50 ms |
| message throughput | 186.72 msg/s | - | - |
Apache-2.0