WebRTC P2P Data Exchange Implementation

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Implementing WebRTC for P2P Data Exchange on Website

WebRTC not limited to video. RTCDataChannel — low-level data channel between browsers with 20–50 ms latency, works over SCTP/DTLS without server mediator for data transmission. File exchanger, collaborative editor, game backend, mesh sync — all built on DataChannel.

RTCDataChannel vs WebSocket

Parameter WebSocket RTCDataChannel
Data route Client → Server → Client Client → Client (P2P)
Latency 50–200 ms (via server) 20–60 ms (direct)
Reliability TCP (ordered, reliable) Configurable
Encryption TLS DTLS (mandatory)
Server load All traffic Signal only

Main advantage — data doesn't go through your servers. Critical for file exchange, E2E-encrypted chats, private game sessions.

Creating DataChannel

// Offerer
const pc = new RTCPeerConnection({ iceServers: [/* ... */] });

const channel = pc.createDataChannel('files', {
  ordered: true,      // TCP semantics
  // maxRetransmits: 0,  // UDP semantics
  // maxPacketLifeTime: 100,
});

channel.binaryType = 'arraybuffer';
channel.bufferedAmountLowThreshold = 65536; // 64 KB

channel.onopen = () => console.log('DataChannel open');
channel.onclose = () => console.log('DataChannel closed');
channel.onmessage = (e) => handleMessage(e.data);

// Answerer
pc.ondatachannel = (e) => {
  const remoteChannel = e.channel;
  remoteChannel.onmessage = (e) => handleMessage(e.data);
};

Reliability Modes

SCTP under DataChannel allows configuring delivery semantics:

  • ordered + reliable (default) — guaranteed order, retransmission. For files, chat.
  • unordered + unreliable (maxRetransmits: 0) — UDP-like. For game positions, cursors.
  • ordered + maxPacketLifeTime — packet lives N ms, then dropped. For voice commands, keyboard input.

File Transfer via DataChannel

Browser DataChannel limited ~256 KB message size. Files need chunking:

const CHUNK_SIZE = 64 * 1024; // 64 KB

async function sendFile(channel, file) {
  const metadata = JSON.stringify({
    name: file.name,
    size: file.size,
    type: file.type,
    chunks: Math.ceil(file.size / CHUNK_SIZE),
  });

  channel.send(metadata);

  const buffer = await file.arrayBuffer();
  let offset = 0;

  function sendNextChunk() {
    while (offset < buffer.byteLength) {
      // Buffer overflow control
      if (channel.bufferedAmount > channel.bufferedAmountLowThreshold * 2) {
        channel.onbufferedamountlow = () => {
          channel.onbufferedamountlow = null;
          sendNextChunk();
        };
        return;
      }

      const chunk = buffer.slice(offset, offset + CHUNK_SIZE);
      channel.send(chunk);
      offset += CHUNK_SIZE;
    }
    channel.send(JSON.stringify({ type: 'transfer-complete' }));
  }

  sendNextChunk();
}

Receiver collects chunks:

let receivedSize = 0;
let receivedChunks = [];
let fileMetadata = null;

channel.onmessage = (e) => {
  if (typeof e.data === 'string') {
    const msg = JSON.parse(e.data);
    if (msg.name) {
      fileMetadata = msg;
    } else if (msg.type === 'transfer-complete') {
      const blob = new Blob(receivedChunks);
      triggerDownload(blob, fileMetadata.name);
    }
  } else {
    receivedChunks.push(e.data);
    receivedSize += e.data.byteLength;
    updateProgress(receivedSize / fileMetadata.size);
  }
};

Progress and Speed

const startTime = Date.now();
let lastSize = 0;

function updateProgress(ratio) {
  const elapsed = (Date.now() - startTime) / 1000;
  const speed = (receivedSize - lastSize) / 1024; // KB/s last tick
  lastSize = receivedSize;

  progressBar.style.width = `${ratio * 100}%`;
  speedLabel.textContent = `${(receivedSize / elapsed / 1024).toFixed(1)} KB/s`;
  etaLabel.textContent = `${((fileMetadata.size - receivedSize) / (receivedSize / elapsed)).toFixed(0)} sec`;
}

Multi-Channel Architecture

Different data types — different channels with different reliability:

const channels = {
  control: pc.createDataChannel('control', { ordered: true }),
  files:   pc.createDataChannel('files',   { ordered: true }),
  cursor:  pc.createDataChannel('cursor',  { ordered: false, maxRetransmits: 0 }),
  chat:    pc.createDataChannel('chat',    { ordered: true }),
};

E2E Encryption over DTLS

DataChannel already DTLS-encrypted. For additional E2E (keys invisible even to server) use Web Crypto API:

// ECDH key exchange during signaling
const keyPair = await crypto.subtle.generateKey(
  { name: 'ECDH', namedCurve: 'P-256' },
  false, ['deriveKey']
);

// Public key sent via signaling server
const publicKeyExported = await crypto.subtle.exportKey('raw', keyPair.publicKey);

// After receiving partner's public key
const sharedKey = await crypto.subtle.deriveKey(
  { name: 'ECDH', public: partnerPublicKey },
  keyPair.privateKey,
  { name: 'AES-GCM', length: 256 },
  false, ['encrypt', 'decrypt']
);

Mesh P2P for Multiple Participants

Up to 4–6 participants — full-mesh topology acceptable (each with each):

class MeshNetwork {
  constructor(signalSocket) {
    this.peers = new Map(); // userId -> RTCPeerConnection
    this.channels = new Map();
    this.signal = signalSocket;
  }

  async connectTo(userId) {
    const pc = new RTCPeerConnection(ICE_CONFIG);
    this.peers.set(userId, pc);

    const channel = pc.createDataChannel('mesh');
    this.channels.set(userId, channel);
    channel.onmessage = (e) => this.onData(userId, e.data);

    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);
    this.signal.emit('offer', { to: userId, offer });
  }

  broadcast(data) {
    const message = JSON.stringify(data);
    this.channels.forEach(ch => {
      if (ch.readyState === 'open') ch.send(message);
    });
  }
}

Typical Applications

Collaborative whiteboard — cursor positions via unreliable channel (~20 ms latency), draw operations via reliable. Delta sync every 100 ms.

Game match 1v1 — player state (position, action) via unordered channel 60 times/sec. Critical events (hit, death) via ordered reliable.

Peer-to-peer chat with files — text via ordered reliable, files chunked with buffer control, image previews as base64 in JSON.

Screenshare + annotation — video stream via RTCPeerConnection, annotations (click coords) via DataChannel.

Limitations and Pitfalls

  • Safari doesn't support bufferedAmountLowThreshold in old versions — need polling via setInterval
  • Firefox max message size 256 KB; Chrome varies by version
  • Mobile browsers may close DataChannel when app backgrounds — need reconnect logic
  • On connection loss iceConnectionState === 'failed' — no auto-recovery, need explicit reconnect

Timeline

  • Basic P2P file exchanger (2 participants) — 3–4 days
  • Multi-user mesh with multiple channels — 1–2 weeks
  • E2E encryption + key exchange — plus 3–4 days
  • Collaborative editor / whiteboard over DataChannel — 2–4 weeks (including CRDT/OT logic)