npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agentdance/node-webrtc

v1.0.4

Published

Production-grade WebRTC in pure TypeScript — RTCPeerConnection, DataChannel, ICE, DTLS, SCTP, SRTP. Zero native bindings.

Downloads

480

Readme

ts-rtc

Production-grade WebRTC implementation in pure TypeScript — zero native bindings, zero C++ glue.

Every protocol layer — ICE, DTLS 1.2, SCTP, SRTP, RTP/RTCP, STUN, and SDP — is built directly from first principles against the relevant RFCs. The public API mirrors the browser's RTCPeerConnection exactly, so Node.js code is portable and drop-in.


Why ts-rtc?

Most Node.js WebRTC libraries are thin wrappers around libwebrtc or libsrtp, making them opaque, hard to audit, and brittle when native builds fail. ts-rtc takes the opposite approach:

| Property | ts-rtc | Native-binding libraries | |---|---|---| | Dependencies | Zero external crypto/TLS libs | libwebrtc, libsrtp, openssl, … | | Debuggability | Step-through any protocol in plain TypeScript | Binary black box | | Auditability | Every algorithm is readable source | Native C++ | | RFC traceability | Inline references to RFC sections | Often undocumented | | Build complexity | pnpm install — nothing to compile | Requires platform toolchain | | Test vectors | RFC-verified test vectors in unit tests | Rarely tested at this level |


Protocol Coverage

| Layer | Standard | Key features | |---|---|---| | ICE | RFC 8445 | Host / srflx / prflx candidates; connectivity checks with retransmit schedule (0 / 200 / 600 / 1400 / 3800 ms); aggressive & regular nomination; 15 s keepalive; BigInt pair-priority per §6.1.2.3 | | DTLS 1.2 | RFC 6347 | Full client+server handshake state machine; ECDHE P-256; AES-128-GCM; self-signed cert via pure ASN.1/DER builder; RFC 5763 §5 role negotiation; 60-byte SRTP key export | | SCTP | RFC 4960 / RFC 8832 | Fragmentation & reassembly; SSN ordering; congestion control (cwnd / ssthresh / slow-start); fast retransmit on 3 duplicate SACKs; SACK gap blocks; FORWARD-TSN; DCEP (RFC 8832); pre-negotiated channels; TSN wrap-around | | SRTP | RFC 3711 | AES-128-CM-HMAC-SHA1-80/32 and AES-128-GCM; RFC-verified key derivation; 64-bit sliding replay window; ROC rollover | | RTP / RTCP | RFC 3550 | Full header codec; CSRC; one-byte & two-byte header extensions; SR / RR / SDES / BYE / NACK / PLI / FIR / REMB / compound packets | | STUN | RFC 5389 | Full message codec; HMAC-SHA1 integrity; CRC-32 fingerprint; ICE attributes (PRIORITY, USE-CANDIDATE, ICE-CONTROLLING, ICE-CONTROLLED) | | SDP | RFC 4566 / WebRTC | Full parse ↔ serialize round-trip; extmap; rtpmap/fmtp; ssrc/ssrc-group; BUNDLE; Chrome interop |


Demo Web Application

A signaling server + demo client that bridges a Flutter macOS app to a Node.js peer.

cd apps/demo-web
pnpm dev    # hot-reload dev server on http://localhost:3000
pnpm start  # production

Demo scenarios

| Scenario | Description | |---|---| | scenario1-multi-file | Multi-file transfer over DataChannel | | scenario2-large-file | Large file transfer with progress reporting | | scenario3-snake | Snake game multiplayer over DataChannel | | scenario4-video | Video streaming |

The signaling server runs WebSocket at ws://localhost:8080/ws with room-based peer discovery.


Architecture

packages/
├── webrtc/    RTCPeerConnection — standard browser API (glue layer)
├── ice/       RFC 8445 ICE agent
├── dtls/      RFC 6347 DTLS 1.2 transport
├── sctp/      RFC 4960 + RFC 8832 SCTP / DCEP
├── srtp/      RFC 3711 SRTP / SRTCP
├── rtp/       RFC 3550 RTP / RTCP codec
├── stun/      RFC 5389 STUN message codec + client
└── sdp/       WebRTC SDP parser / serializer

apps/
├── demo-web/       Express + WebSocket signaling server (4 demo scenarios)
├── bench/          500 MB DataChannel throughput benchmark
└── demo-flutter/   Flutter macOS client (flutter_webrtc)

features/           Cucumber BDD acceptance tests (living specification)

Each package is independently importable. @ts-rtc/webrtc is the only package most consumers need.


Quickstart

Prerequisites

  • Node.js 18+
  • pnpm

Install

git clone https://github.com/your-org/ts-rtc.git
cd ts-rtc
pnpm install

Build

pnpm build          # compile all packages to dist/

Run tests

pnpm test           # Vitest unit tests across all packages
pnpm test:bdd       # Cucumber BDD acceptance tests

Usage

Minimal DataChannel (peer-to-peer in Node.js)

import { RTCPeerConnection } from '@ts-rtc/webrtc';

// ── Offerer ───────────────────────────────────────────────────────────────────
const pcA = new RTCPeerConnection({ iceServers: [] });
const dc  = pcA.createDataChannel('chat');

dc.on('open',    ()    => dc.send('Hello WebRTC!'));
dc.on('message', data  => console.log('[A received]', data));

// ── Answerer ──────────────────────────────────────────────────────────────────
const pcB = new RTCPeerConnection({ iceServers: [] });

pcB.on('datachannel', channel => {
  channel.on('message', data => {
    console.log('[B received]', data);
    channel.send('Hello back!');
  });
});

// ── Trickle ICE ───────────────────────────────────────────────────────────────
pcA.on('icecandidate', c => c && pcB.addIceCandidate(c));
pcB.on('icecandidate', c => c && pcA.addIceCandidate(c));

// ── SDP exchange ──────────────────────────────────────────────────────────────
const offer  = await pcA.createOffer();
await pcA.setLocalDescription(offer);
await pcB.setRemoteDescription(offer);

const answer = await pcB.createAnswer();
await pcB.setLocalDescription(answer);
await pcA.setRemoteDescription(answer);

With a STUN server

const pc = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});

Binary data

const buf = crypto.randomBytes(65536);
dc.on('open', () => dc.send(buf));

remoteChannel.on('message', (data: Buffer) => {
  console.log('received', data.byteLength, 'bytes');
});

Multiple concurrent channels

const ctrl = pcA.createDataChannel('control', { ordered: true });
const bulk = pcA.createDataChannel('bulk',    { ordered: false });
const log  = pcA.createDataChannel('log',     { maxRetransmits: 0 });

Pre-negotiated channel (no DCEP round-trip)

// Both peers must call this with the same id
const chA = pcA.createDataChannel('secure', { negotiated: true, id: 5 });
const chB = pcB.createDataChannel('secure', { negotiated: true, id: 5 });

Backpressure-aware large transfers

const CHUNK  = 1168;          // one SCTP DATA payload (fits within PMTU)
const HIGH   = 4 * 1024 * 1024;
const LOW    = 2 * 1024 * 1024;

dc.bufferedAmountLowThreshold = LOW;

function pump(data: Buffer, offset = 0) {
  while (offset < data.length) {
    if (dc.bufferedAmount > HIGH) {
      dc.once('bufferedamountlow', () => pump(data, offset));
      return;
    }
    dc.send(data.subarray(offset, offset + CHUNK));
    offset += CHUNK;
  }
}

dc.on('open', () => pump(largeBuffer));

Connection state monitoring

pc.on('connectionstatechange', () => {
  console.log('connection:', pc.connectionState);
  // 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed'
});

pc.on('iceconnectionstatechange', () => {
  console.log('ICE:', pc.iceConnectionState);
});

pc.on('icegatheringstatechange', () => {
  console.log('gathering:', pc.iceGatheringState);
});

Stats

const stats = await pc.getStats();
for (const [, entry] of stats) {
  if (entry.type === 'candidate-pair' && entry.nominated) {
    console.log('RTT:', entry.currentRoundTripTime);
    console.log('bytes sent:', entry.bytesSent);
  }
}

Graceful close

await dc.close();
pc.close();

RTCPeerConnection API Reference

Constructor

new RTCPeerConnection(config?: RTCConfiguration)

| Option | Type | Default | |---|---|---| | iceServers | RTCIceServer[] | [{ urls: 'stun:stun.l.google.com:19302' }] | | iceTransportPolicy | 'all' \| 'relay' | 'all' | | bundlePolicy | 'max-bundle' \| 'balanced' \| 'max-compat' | 'max-bundle' | | rtcpMuxPolicy | 'require' | 'require' | | iceCandidatePoolSize | number | 0 |

Methods

| Method | Description | |---|---| | createOffer() | Generate an SDP offer | | createAnswer() | Generate an SDP answer | | setLocalDescription(sdp) | Apply local SDP, begin ICE gathering | | setRemoteDescription(sdp) | Apply remote SDP, begin ICE connectivity checks | | addIceCandidate(candidate) | Feed a trickled ICE candidate | | createDataChannel(label, init?) | Create a DataChannel | | addTransceiver(kind, init?) | Add an RTP transceiver | | getTransceivers() | List all transceivers | | getSenders() | List RTP senders | | getReceivers() | List RTP receivers | | getStats() | Retrieve RTCStatsReport | | restartIce() | Trigger ICE restart | | close() | Tear down the connection |

Events

| Event | Payload | When | |---|---|---| | icecandidate | RTCIceCandidateInit \| null | New local ICE candidate; null = gathering complete | | icecandidateerror | { errorCode, errorText } | STUN server unreachable | | iceconnectionstatechange | — | ICE connection state changed | | icegatheringstatechange | — | ICE gathering state changed | | connectionstatechange | — | Overall connection state changed | | signalingstatechange | — | Signaling state changed | | negotiationneeded | — | Re-negotiation required | | datachannel | RTCDataChannel | Remote opened a DataChannel | | track | RTCTrackEvent | Remote RTP track received |


RTCDataChannel API Reference

Properties

| Property | Type | Description | |---|---|---| | label | string | Channel name | | readyState | 'connecting' \| 'open' \| 'closing' \| 'closed' | Current state | | ordered | boolean | Reliable ordering | | maxPacketLifeTime | number \| null | Partial reliability (ms) | | maxRetransmits | number \| null | Partial reliability (count) | | protocol | string | Sub-protocol | | negotiated | boolean | Pre-negotiated (no DCEP) | | id | number | SCTP stream ID | | bufferedAmount | number | Bytes queued in send buffer | | bufferedAmountLowThreshold | number | Threshold for bufferedamountlow | | binaryType | 'arraybuffer' | Binary message format |

Methods

| Method | Description | |---|---| | send(data) | Send string \| Buffer \| ArrayBuffer \| ArrayBufferView | | close() | Close the channel |

Events

| Event | Payload | When | |---|---|---| | open | — | Channel ready to send | | message | string \| Buffer | Message received | | close | — | Channel closed | | closing | — | Close initiated | | error | Error | Channel error | | bufferedamountlow | — | Buffered amount crossed threshold |


Lower-Level Package APIs

Each protocol layer is independently usable for specialized use-cases.

@ts-rtc/ice — ICE Agent

import { IceAgent } from '@ts-rtc/ice';

const agent = new IceAgent({ role: 'controlling', iceServers: [] });
await agent.gather();
agent.setRemoteParameters({ usernameFragment: '…', password: '…' });
agent.addRemoteCandidate(candidate);
await agent.connect();
agent.send(Buffer.from('data'));
agent.on('data', (buf) => console.log(buf));

@ts-rtc/dtls — DTLS 1.2 Transport

import { DtlsTransport } from '@ts-rtc/dtls';

const dtls = new DtlsTransport(iceTransport, {
  role: 'client',                // or 'server'
  remoteFingerprint: { algorithm: 'sha-256', value: '…' },
});
await dtls.start();
dtls.on('connected', () => {
  const keys = dtls.getSrtpKeyingMaterial(); // { clientKey, serverKey, clientSalt, serverSalt }
});
dtls.send(Buffer.from('app data'));

@ts-rtc/sctp — SCTP Association

import { SctpAssociation } from '@ts-rtc/sctp';

const sctp = new SctpAssociation(dtlsTransport, { role: 'client', port: 5000 });
await sctp.connect();
const channel = await sctp.createDataChannel('chat');
channel.send('hello');
sctp.on('datachannel', (ch) => ch.on('message', console.log));

@ts-rtc/srtp — SRTP Protect / Unprotect

import { createSrtpContext, srtpProtect, srtpUnprotect } from '@ts-rtc/srtp';
import { ProtectionProfile } from '@ts-rtc/srtp';

const ctx = createSrtpContext(ProtectionProfile.AES_128_CM_HMAC_SHA1_80, keyingMaterial);
const protected_  = srtpProtect(ctx,   rtpPacket);
const unprotected = srtpUnprotect(ctx, protected_);

@ts-rtc/stun — STUN Codec

import { encodeMessage, decodeMessage, createBindingRequest } from '@ts-rtc/stun';

const req = createBindingRequest({ username: 'user:pass', priority: 12345 });
const buf = encodeMessage(req, 'password');
const msg = decodeMessage(buf);

@ts-rtc/sdp — SDP Parser / Serializer

import { parse, serialize, parseCandidate } from '@ts-rtc/sdp';

const session = parse(sdpString);
const text    = serialize(session);
const cand    = parseCandidate('candidate:…');

@ts-rtc/rtp — RTP / RTCP Codec

import { encodeRtp, decodeRtp, encodeRtcpSr, decodeRtcp } from '@ts-rtc/rtp';

const packet = encodeRtp({ payloadType: 96, sequenceNumber: 1, timestamp: 0, ssrc: 42, payload });
const { header, payload } = decodeRtp(packet);

Throughput Benchmark

Measures raw DataChannel throughput on a Node.js loopback — no network, pure protocol stack cost.

cd apps/bench
../../node_modules/.bin/tsx bench.ts

What it tests:

  • Two isolated Node.js processes (sender and receiver) connected via IPC-bridged signaling
  • 500 MB binary transfer in 1168-byte chunks (matches SCTP DATA payload size for a 1200-byte PMTU)
  • Backpressure via bufferedAmountLowThreshold (high-watermark 4 MB, low-watermark 2 MB)
  • SHA-256 end-to-end integrity verification — the benchmark fails if a single byte is wrong

Sample output:

════════════════════════════════════════════════════════════
  ts-rtc 500MB DataChannel Throughput Benchmark
  Path: Node.js loopback (127.0.0.1)
════════════════════════════════════════════════════════════
  Benchmark complete
  SHA-256 verification: ✅ passed
  Transfer time:        8.3 s
  Average speed:        60.24 MB/s
  Total wall time:      9.1 s
════════════════════════════════════════════════════════════

Test Suite

Unit tests — Vitest

pnpm test

| Package | Test file | Key coverage | |---|---|---| | webrtc | webrtc.test.ts (604 lines) | RTCPeerConnection lifecycle, SDP factory, DTLS role negotiation, full ICE+DTLS+SCTP loopback | | ice | ice.test.ts (555 lines) | Candidate priority/foundation math, pair formation, loopback connectivity, restart, tier classification | | dtls | dtls.test.ts (738 lines) | Record codec, handshake messages, PRF vectors, self-signed cert, AES-GCM, full loopback, both-client deadlock regression | | sctp | association.test.ts (436 lines) | Handshake, DCEP, 65536B + 4 MiB transfers, cwnd growth, peerRwnd, flightSize, backpressure, pre-negotiated, ordered/unordered, 3 concurrent channels, TSN wrap-around | | srtp | srtp.test.ts (609 lines) | RFC 3711 §B.2 keystream vectors, §B.3 key derivation vectors, HMAC-SHA1, ReplayWindow, protect+unprotect, tamper detection, ROC wrap | | rtp | rtp.test.ts (570 lines) | RTP encode/decode, CSRC, header extensions, all RTCP types, compound packets, sequence wrap, NTP conversion | | sdp | sdp.test.ts (827 lines) | Chrome offer/answer parsing, round-trip fidelity, all candidate types, fingerprint, directions, SSRC groups, extmap | | stun | stun.test.ts (569 lines) | All attribute types, XOR-MAPPED-ADDRESS (IPv4 + IPv6), MESSAGE-INTEGRITY (correct/wrong/tampered), FINGERPRINT, ICE attributes |

Total: ~4,900 lines of unit tests across 9 test files.

BDD acceptance tests — Cucumber.js

pnpm test:bdd

29 scenarios across 5 feature files:

| Feature file | Scenarios | What it covers | |---|---|---| | webrtc/peer-connection.feature | 14 | Basic negotiation, bidirectional messaging, binary data, 65 KB fragmentation, 3 concurrent channels, late channel creation, pre-negotiated, unordered, close, signaling state machine, getStats, 4 MiB end-to-end byte-integrity transfer | | webrtc/dtls-role-interop.feature | 7 | RFC 5763 §5 role negotiation (actpass / active / passive), complementary role assignment, data over negotiated connection, both-client deadlock regression | | ice/ice-connectivity.feature | 2 | ICE gathering (valid candidates), ICE loopback connectivity | | dtls/dtls-handshake.feature | 2 | DTLS loopback handshake, matching SRTP keying material, app data exchange | | sctp/sctp-channels.feature | 4 | SCTP handshake, DCEP open, 65 KiB binary transfer, 4 MiB binary transfer |

Reports are written to reports/cucumber-report.html.


Other Commands

pnpm typecheck   # TypeScript strict-mode check across all packages (no emit)
pnpm lint        # ESLint 9 + @typescript-eslint
pnpm clean       # Remove all dist/ directories

TypeScript Configuration

All packages share tsconfig.base.json:

{
  "target": "ES2022",
  "module": "NodeNext",
  "strict": true,
  "exactOptionalPropertyTypes": true,
  "noUncheckedIndexedAccess": true,
  "noImplicitOverride": true
}

exactOptionalPropertyTypes and noUncheckedIndexedAccess are enabled intentionally — they catch protocol-level bugs at compile time that strict mode alone misses.


Monorepo Layout

ts-rtc/
├── packages/          # Protocol stack (each independently publishable)
├── apps/              # Demo and benchmark applications
├── features/          # Cucumber BDD specs + step definitions
├── package.json       # pnpm workspace root
├── pnpm-workspace.yaml
├── tsconfig.base.json # Shared compiler options
└── cucumber.yaml      # BDD runner config

Design Principles

  1. No native dependencies. Everything is implemented in TypeScript using only node:crypto, node:dgram, and node:net. No OpenSSL bindings, no node-gyp, no pre-built binaries.

  2. RFC first. Every algorithm includes inline RFC section references. If behavior diverges from the spec, it is a bug.

  3. Layered, independently testable. ICE, DTLS, SCTP, and SRTP are separate packages that can be tested in isolation. The full WebRTC stack is integration-tested at the @ts-rtc/webrtc layer.

  4. Backpressure everywhere. bufferedAmount and bufferedAmountLowThreshold are plumbed from SCTP congestion control all the way through DCEP to RTCDataChannel, enabling safe high-throughput transfers without unbounded memory growth.

  5. Test vectors over trust. Cryptographic primitives (AES-CM keystream, HMAC-SHA1 key derivation, CRC-32 fingerprint) are verified against the exact vectors published in their respective RFCs.


License

MIT