Skip to content
/ p2p Public

A lightweight library for creating peer-to-peer WebRTC conferencing with custom signaling drivers

License

Notifications You must be signed in to change notification settings

meefik/p2p

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

p2p

A tiny, signaling-agnostic library for building peer-to-peer WebRTC conferencing (media + data channels) with pluggable signaling drivers.

Key ideas:

  • Sender: creates outgoing RTCPeerConnections, publishes a local MediaStream, and optionally opens data channels to receivers.
  • Receiver: listens for offers, answers them, and exposes remote MediaStreams and incoming data messages.
  • Signaling driver: any object implementing on(namespace, handler), off(namespace, handler) and emit(namespace, message). This keeps the library transport-agnostic (WebSocket, pub/sub, in-memory, etc).

Why use p2p:

  • Minimal footprint and API surface for broadcasting a local stream and exchanging data.
  • Easy to test locally with an in-memory driver; swap to WebSocket or other drivers for production.
  • Handles ICE, offer/answer exchange, candidate buffering, and per-peer data channels.

Quickstart

Install:

npm install p2p

Run the demo (clone the repo if needed):

npm run dev

Open http://localhost:8000/demo/ in two browser tabs to see a simple video chat demo.

DEMO

Basic usage

Minimal in-memory signaling driver (useful for local testing):

// 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, message) {
    const k = namespace.join(':');
    if (!this.has(k)) return;
    for (const h of this.get(k)) {
      try {
        h(message);
      } catch (e) {
        /* swallow errors */
      }
    }
  }
}

Signaling namespaces (contract)

  • 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]

Message types

  • invoke — discovery / request to connect (contains id, optional credentials)
  • offer — sender -> receiver with SDP offer and metadata
  • answer — receiver -> sender with SDP answer
  • candidate — ICE candidate exchange
  • dispose — end/tear-down

Receiver — listen for senders and attach incoming streams:

import { Receiver } from 'p2p';

const driver = new MemoryDriver();
const receiver = new Receiver({ driver });

receiver.addEventListener('stream', (e) => {
  const { id, stream, metadata } = e.detail;
  console.log('received stream from', id, metadata);

  // attach the received stream to a video element
  const video = document.createElement('video');
  video.autoplay = true;
  video.srcObject = stream;
  video.dataset.source = metadata.source || 'unknown';
  document.body.appendChild(video);
});

receiver.addEventListener('channel:message', (e) => {
  const { id, channel, data } = e.detail;
  console.log('msg from', id, channel.label, data);

  if (channel.label === 'chat') {
    console.log('chat message:', data);
    // respond to chat messages
    channel.send('ping');
  }
});

receiver.start({ room: 'demo-room' });
// receiver.stop();

Sender — capture local media, broadcast, and send messages:

import { Sender } from 'p2p';

const driver = new MemoryDriver();
const sender = new Sender({ driver });

sender.addEventListener('connect', (e) => {
  const { id } = e.detail;
  console.log('peer connected', id);
});

sender.addEventListener('channel:open', (e) => {
  const { id, channel } = e.detail;
  console.log('data channel opened', id, channel.label);

  if (channel.label === 'chat') {
    // send a message to the data channel
    channel.send('ping');
  }
});

sender.addEventListener('channel:message', (e) => {
  const { id, channel, data } = e.detail;
  console.log('msg from', id, channel.label, data);

  if (channel.label === 'chat') {
    // read a chat message from the data channel
    console.log('chat message:', data);
  }
});

navigator.mediaDevices.getUserMedia({ audio: true, video: true })
  .then((stream) => {
    sender.start({
      room: 'demo-room',
      stream,
      channels: {
        chat: { ordered: true }
      },
      metadata: { source: 'camera' },
    });
    // sender.stop();
  });

You can send a message to all connected peers via the 'chat' data channel:

sender.connections.forEach((conn) => {
  const channel = conn.channels.get('chat');
  if (channel && channel.readyState === 'open') {
    channel.send('hello peers!');
  }
});

API summary

Sender:

  • constructor(config: { driver, iceServers?, verify?, connectionTimeout?, audioBitrate?, videoBitrate? })
  • start({ room?, stream?, channels?, metadata? })
  • stop()

Events: connect, dispose, error, channel:open, channel:close, channel:error, channel:message

Receiver:

  • constructor(config: { driver, iceServers?, connectionTimeout?, pingInterval?, pingAttempts? })
  • start({ room?, credentials? })
  • stop()

Events: stream, connect, dispose, channel:open, channel:close, channel:error, channel:message

Troubleshooting & tips

  • Browser permissions: getUserMedia requires secure context (https or localhost) and user consent.
  • TURN servers: include TURN servers in iceServers for reliable connectivity across NATs.
  • Candidate buffering: the library buffers ICE candidates received before a connection is created.
  • Bitrate and codec hints: the library sets preferred codecs and bitrate where supported; browsers may ignore hints.
  • Debugging: use browser WebRTC internals and ICE/state events to diagnose connectivity issues.

Security & verification

  • Sender supports an optional verify() callback to accept/reject incoming invocations.
  • If you need authentication/authorization, implement it in your signaling layer and/or verify callback.
  • Always use secure signaling channels (e.g., WSS) to protect exchanged SDP and ICE candidates.

About

A lightweight library for creating peer-to-peer WebRTC conferencing with custom signaling drivers

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published