tinypeer
v0.2.0
Published
Tiny, modern WebRTC library compatible with PeerJS servers
Downloads
100
Maintainers
Readme
TinyPeer
[!WARNING]
This project is in early development. The API may change before a stable 1.0 release. Use with caution in production.
A minimalistic, peer-to-peer WebRTC library for the browser. Compatible with PeerJS servers.
A younger sibling to PeerJS focused on simplicity, modern features, and a tiny bundle size.
Why TinyPeer?
- Simple - Functional design, minimal API surface
- Modern - ES2020+, TypeScript, ESM-only, Promise-based
- Full-featured - Data channels + media streaming (audio/video)
- Tiny - ~4.19 KB minified + gzipped
- Compatible - Works with existing PeerJS servers
- Zero dependencies
Build multiplayer games, collaborative tools, video chat apps, and real-time experiences without a backend.
Installation
npm install tinypeerQuick Start
Client connecting to peer
import { createPeer } from 'tinypeer'
const peer = await createPeer()
const conn = await peer.connect('host-peer-id', {
metadata: { username: 'Alice' }
})
await conn.send({ message: 'Hello!' })
conn.on('data', (data) => {
console.log('Received:', data)
})Host accepting connections
import { createPeer } from 'tinypeer'
const peer = await createPeer({ id: 'host-peer-id' })
peer.on('connection', async (conn) => {
console.log(`${conn.peerId } connected`)
await conn.send({ message: 'Welcome!' })
conn.on('data', (data) => {
console.log('Received:', data)
})
})
console.log(`Listening with ID: ${peer.id}`)API
createPeer(options?): Promise<Peer>
Creates a new peer instance. Resolves when connected to the signaling server.
const peer = await createPeer({
id: 'my-peer', // Optional: auto-generated if not provided
host: '0.peerjs.com', // Optional: PeerJS server host
port: 443, // Optional: PeerJS server port
path: '/peerjs', // Optional: PeerJS server path
key: 'peerjs', // Optional: API key
secure: true, // Optional: Use WSS/HTTPS
pingInterval: 5000, // Optional: Heartbeat interval in ms
rtcConfig: {...}, // Optional: Custom RTCConfiguration
})Peer
Properties:
id- Your peer ID
Methods:
connect(peerId, options?)- Connect to another peercall(peerId, stream, options?)- Call another peer with mediadisconnect()- Close signaling connectiondestroy()- Close all connectionson('connection', handler)- Handle incoming connectionson('call', handler)- Handle incoming calls
peer.connect(peerId, options?): Promise<Connection>
Initiates a connection to another peer. Resolves when the connection is open and ready.
const conn = await peer.connect('other-peer', {
metadata: { username: 'Alice' }, // Optional: custom metadata
reliable: true, // Use reliable/ordered data channel
})peer.call(peerId, stream, options?): MediaConnection
Initiates a media call to another peer. Returns immediately with a MediaConnection object. The stream promise resolves when the remote peer answers and media is received.
const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
const call = peer.call('other-peer', localStream, {
metadata: { username: 'Alice' }
})
const remoteStream = await call.streampeer.on('connection', handler)
Fires when another peer connects to you for data. The connection is already open when this event fires.
peer.on('connection', async (conn) => {
console.log(`${conn.peer} connected`)
console.log('Metadata:', conn.metadata)
await conn.send({ welcome: true })
})peer.on('call', handler)
Fires when another peer calls you for media. The call is ready but not yet answered.
peer.on('call', async (call) => {
console.log(`Call from ${call.peer}`)
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
await call.answer(stream)
const remoteStream = await call.stream
})peer.disconnect()
Closes the signaling connection but keeps peer-to-peer connections alive.
peer.destroy()
Closes all connections and disconnects from the signaling server.
Connection
Properties:
peer- Remote peer IDmetadata- Optional connection metadata
Methods:
send(data, metadata?)- Send data to peerclose()- Close the connectionon('data', handler)- Handle incoming dataon('close', handler)- Handle connection closeon('error', handler)- Handle errors
connection.send(data, metadata?): Promise<void>
Sends data with optional metadata. Supports JSON objects, strings, and binary data (ArrayBuffer/TypedArray).
// Send JSON
await connection.send({ type: 'move', x: 10, y: 20 })
// Send binary with metadata
const imageData = new Uint8Array(imageBytes)
await connection.send(imageData, {
filename: 'photo.jpg',
type: 'image/jpeg'
})Note: Binary data cannot be nested in JSON. Use the metadata parameter instead:
// ❌ Won't work
await connection.send({ image: new Uint8Array([...]) })
// ✅ Works
await connection.send(new Uint8Array([...]), { type: 'image' })connection.on('data', handler)
Fires when data is received from the remote peer.
connection.on('data', (data, metadata) => {
console.log('Received:', data)
// Handle different data types
if (data instanceof Uint8Array) {
console.log('Binary data received:', data.byteLength, 'bytes')
if (metadata?.type === 'image/jpeg') {
displayImage(data, metadata.filename)
}
} else if (typeof data === 'string') {
console.log('String:', data)
} else {
console.log('JSON object:', data)
}
})connection.on('close', handler)
Fires when the connection closes.
connection.on('error', handler)
Fires when a connection error occurs.
connection.close()
Closes the connection.
MediaConnection
Properties:
peer- Remote peer IDmetadata- Optional call metadatastream- Promise that resolves with remote MediaStream
Methods:
answer(stream?)- Answer the callreject()- Reject the callclose()- End the callon('stream', handler)- Handle remote streamon('close', handler)- Handle call closeon('error', handler)- Handle errors
call.stream: Promise<MediaStream>
Promise that resolves with the remote media stream. Rejects if call fails or is rejected.
try {
const remoteStream = await call.stream
videoEl.srcObject = remoteStream
} catch (error) {
console.log('Call failed:', error.message)
}call.answer(stream?): Promise<void>
Answer an incoming call, optionally with a local stream.
// With local stream
const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
await call.answer(localStream)
// Without local stream (receive-only)
await call.answer()call.reject(): Promise<void>
Reject an incoming call. The caller's call.stream promise will reject.
peer.on('call', async (call) => {
if (!userAccepts) {
await call.reject()
}
})call.on('stream', handler)
Fires when remote stream is received.
call.on('close', handler)
Fires when the call closes.
call.on('error', handler)
Fires when a call error occurs.
call.close()
End the call.
Examples
Video Call
import { createPeer } from 'tinypeer'
// Caller
const caller = await createPeer({ id: 'alice' })
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
})
localVideoEl.srcObject = localStream
const call = caller.call('bob', localStream)
const remoteStream = await call.stream
remoteVideoEl.srcObject = remoteStream
// Callee (separate peer)
const callee = await createPeer({ id: 'bob' })
callee.on('call', async (call) => {
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
})
await call.answer(localStream)
const remoteStream = await call.stream
remoteVideoEl.srcObject = remoteStream
})
// Variations:
// Audio only: { audio: true, video: false }
// Screen share: navigator.mediaDevices.getDisplayMedia({ video: true })
// Receive only: await call.answer() without a streamBroadcasting to Multiple Peers
import { createPeer } from 'tinypeer'
const host = await createPeer({ id: 'room-host' })
const connections = new Map()
host.on('connection', async (conn) => {
connections.set(conn.peer, conn)
conn.on('data', (data) => {
// Broadcast to all other peers
for (const [id, connection] of connections) {
if (id !== conn.peer) {
connection.send({ from: conn.peer, data })
}
}
})
conn.on('close', () => {
connections.delete(conn.peer)
})
})For more examples, see the examples/ directory.
Advanced
Custom Data Encoding
For advanced serialization (MessagePack, CBOR, etc.), compression, or encryption, see Custom Codecs.
Data-Only Peer
For applications that only need data channels (no video/audio), you can use createDataPeer() for a smaller bundle size:
import { createDataPeer } from 'tinypeer'
const peer = await createDataPeer({ id: 'my-peer' })
// All connection features work the same
const conn = await peer.connect('other-peer')
await conn.send({ message: 'Hello!' })
peer.on('connection', (conn) => {
console.log(`${conn.peer} connected`)
})Benefits:
- Smaller bundle size 3.37KB (no media connection code)
- Same API as
createPeer()for data connections - Perfect for multiplayer games, chat apps, collaborative tools
What's different:
- No
call()method - data connections only - No
on('call')event - won't receive media calls
Migrating from PeerJS
See the Migration Guide for API differences and examples.
Browser Support
Aims to support all modern browsers with WebRTC support.
Part of Peer.js's bundle size comes from support for old browsers and stabilising the WebRTC APIs across browsers. TinyPeer uses modern APIs and assumes a recent browser version (ES2020+).
If there is a notable browser quirk or bug, please open an issue.
License
MIT
Acknowledgements
Inspired by:
