p5.js Expert Article 7

WebRTC & Live Collaboration

Peer-to-peer video, real-time data channels, and building shared-canvas experiences with PeerJS and WebRTC.

⏱ 26 min read WebRTC PeerJS collaboration multiplayer data channels video

What is WebRTC?

WebRTC (Web Real-Time Communication) is a browser API for peer-to-peer media and data. Unlike HTTP (server intermediary), WebRTC sends data directly between browsers — low latency, no server bandwidth costs.

Two use cases for creative coding:

  1. Video/audio streams — receiving another person’s camera as a texture
  2. Data channels — sending cursor positions, events, or canvas state in real time

PeerJS — simpler WebRTC

PeerJS wraps the complex WebRTC API:

<script src="https://unpkg.com/peerjs@1.5.4/dist/peerjs.min.js"></script>

PeerJS still needs a signalling server (to exchange connection info before the direct connection is established). PeerJS provides a free cloud server for development; deploy your own for production.

Basic data channel — shared cursor

Sender (broadcaster):

let peer, conn;
let myId = 'broadcaster-' + floor(random(10000));

function setup() {
  createCanvas(700, 500);
  peer = new Peer(myId);

  peer.on('connection', c => {
    conn = c;
    print('Connected:', c.peer);
  });

  print('My ID:', myId);
}

function draw() {
  background(20);
  fill(255, 100, 50);
  noStroke();
  circle(mouseX, mouseY, 20);

  if (conn && conn.open) {
    conn.send({ x: mouseX, y: mouseY, t: frameCount });
  }
}

Receiver (viewer):

let peer, conn;
let remote = { x: 0, y: 0 };

function setup() {
  createCanvas(700, 500);
  peer = new Peer();

  peer.on('open', id => {
    print('My ID:', id);
    conn = peer.connect('broadcaster-XXXX');  // sender's ID
    conn.on('data', data => { remote = data; });
  });
}

function draw() {
  background(20);
  fill(100, 200, 255);
  noStroke();
  circle(remote.x, remote.y, 20);
}

Shared drawing canvas

Two or more users drawing on the same canvas:

let peer, connections = [];
let strokes = [];

function setup() {
  createCanvas(800, 600);
  background(250);

  peer = new Peer();
  peer.on('open', id => {
    print('Your room code:', id);
    document.title = id;
  });

  peer.on('connection', conn => {
    setupConn(conn);
  });
}

function setupConn(conn) {
  connections.push(conn);
  conn.on('data', data => {
    if (data.type === 'stroke') {
      applyStroke(data);
    }
  });
}

function joinRoom(hostId) {
  let conn = peer.connect(hostId);
  setupConn(conn);
}

function mouseDragged() {
  let stroke = {
    type: 'stroke',
    x1: pmouseX, y1: pmouseY,
    x2: mouseX,  y2: mouseY,
    colour: myColour,
    weight: brushSize
  };

  applyStroke(stroke);
  broadcast(stroke);
}

function applyStroke(s) {
  stroke(s.colour);
  strokeWeight(s.weight);
  line(s.x1, s.y1, s.x2, s.y2);
}

function broadcast(data) {
  for (let c of connections) {
    if (c.open) c.send(data);
  }
}

Video as a canvas texture

Receive a peer’s webcam and render it as a p5.js texture:

let peer, remoteVideo;

function setup() {
  createCanvas(640, 480);

  peer = new Peer();

  navigator.mediaDevices.getUserMedia({ video: true, audio: true })
    .then(stream => {
      peer.on('call', call => {
        call.answer(stream);
        call.on('stream', remoteStream => {
          remoteVideo = createElement('video');
          remoteVideo.elt.srcObject = remoteStream;
          remoteVideo.elt.play();
          remoteVideo.hide();
        });
      });
    });
}

function draw() {
  background(20);

  if (remoteVideo && remoteVideo.elt.readyState >= 2) {
    image(remoteVideo, 0, 0, width, height);
  }
}

Multi-user state synchronisation

For installations with many participants, broadcast game state rather than individual events:

// State reconciliation pattern
let localState  = {};
let remoteState = {};
let displayedState = {};

function update() {
  // Blend local and remote states
  for (let key in remoteState) {
    if (!displayedState[key]) displayedState[key] = remoteState[key];
    displayedState[key] = lerp(displayedState[key], remoteState[key], 0.2);
  }
}

// Send state every N frames
if (frameCount % 3 === 0 && conn && conn.open) {
  conn.send({ type: 'state', data: localState });
}

// Receive
conn.on('data', msg => {
  if (msg.type === 'state') remoteState = msg.data;
});

Production considerations

  • Signalling server: PeerJS Cloud is free but limited. Deploy peerjs-server on a VPS for reliability
  • NAT traversal: WebRTC uses STUN/TURN servers. PeerJS includes free STUN; TURN servers (for relaying behind restrictive firewalls) require your own or a service like Twilio
  • Data channel limits: each PeerJS data message is JSON-serialised; for high-frequency data, keep payloads small
  • Reconnection: always handle conn.on('close') and conn.on('error') to reconnect gracefully

Key takeaways

  • WebRTC enables peer-to-peer data and video with low latency
  • PeerJS abstracts the WebRTC handshake — peers connect via an ID
  • Data channels send arbitrary JSON between peers; use them for cursor sync, drawing events, or game state
  • Remote video streams can be drawn to the canvas with image(remoteVideo, 0, 0)
  • For multi-user sync, send compressed state snapshots rather than every event
  • Deploy your own PeerJS signalling server for production reliability