@agentdance/node-webrtc 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +558 -0
- package/package.json +9 -8
package/README.md
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# ts-rtc
|
|
2
|
+
|
|
3
|
+
**Production-grade WebRTC implementation in pure TypeScript — zero native bindings, zero C++ glue.**
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why ts-rtc?
|
|
10
|
+
|
|
11
|
+
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:
|
|
12
|
+
|
|
13
|
+
| Property | ts-rtc | Native-binding libraries |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Dependencies | Zero external crypto/TLS libs | `libwebrtc`, `libsrtp`, `openssl`, … |
|
|
16
|
+
| Debuggability | Step-through any protocol in plain TypeScript | Binary black box |
|
|
17
|
+
| Auditability | Every algorithm is readable source | Native C++ |
|
|
18
|
+
| RFC traceability | Inline references to RFC sections | Often undocumented |
|
|
19
|
+
| Build complexity | `pnpm install` — nothing to compile | Requires platform toolchain |
|
|
20
|
+
| Test vectors | RFC-verified test vectors in unit tests | Rarely tested at this level |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Protocol Coverage
|
|
25
|
+
|
|
26
|
+
| Layer | Standard | Key features |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| **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 |
|
|
29
|
+
| **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 |
|
|
30
|
+
| **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 |
|
|
31
|
+
| **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 |
|
|
32
|
+
| **RTP / RTCP** | RFC 3550 | Full header codec; CSRC; one-byte & two-byte header extensions; SR / RR / SDES / BYE / NACK / PLI / FIR / REMB / compound packets |
|
|
33
|
+
| **STUN** | RFC 5389 | Full message codec; HMAC-SHA1 integrity; CRC-32 fingerprint; ICE attributes (PRIORITY, USE-CANDIDATE, ICE-CONTROLLING, ICE-CONTROLLED) |
|
|
34
|
+
| **SDP** | RFC 4566 / WebRTC | Full parse ↔ serialize round-trip; extmap; rtpmap/fmtp; ssrc/ssrc-group; BUNDLE; Chrome interop |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Demo Web Application
|
|
39
|
+
|
|
40
|
+
A signaling server + demo client that bridges a Flutter macOS app to a Node.js peer.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd apps/demo-web
|
|
44
|
+
pnpm dev # hot-reload dev server on http://localhost:3000
|
|
45
|
+
pnpm start # production
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Demo scenarios
|
|
49
|
+
|
|
50
|
+
| Scenario | Description |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `scenario1-multi-file` | Multi-file transfer over DataChannel |
|
|
53
|
+
| `scenario2-large-file` | Large file transfer with progress reporting |
|
|
54
|
+
| `scenario3-snake` | Snake game multiplayer over DataChannel |
|
|
55
|
+
| `scenario4-video` | Video streaming |
|
|
56
|
+
|
|
57
|
+
The signaling server runs WebSocket at `ws://localhost:8080/ws` with room-based peer discovery.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Architecture
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
packages/
|
|
65
|
+
├── webrtc/ RTCPeerConnection — standard browser API (glue layer)
|
|
66
|
+
├── ice/ RFC 8445 ICE agent
|
|
67
|
+
├── dtls/ RFC 6347 DTLS 1.2 transport
|
|
68
|
+
├── sctp/ RFC 4960 + RFC 8832 SCTP / DCEP
|
|
69
|
+
├── srtp/ RFC 3711 SRTP / SRTCP
|
|
70
|
+
├── rtp/ RFC 3550 RTP / RTCP codec
|
|
71
|
+
├── stun/ RFC 5389 STUN message codec + client
|
|
72
|
+
└── sdp/ WebRTC SDP parser / serializer
|
|
73
|
+
|
|
74
|
+
apps/
|
|
75
|
+
├── demo-web/ Express + WebSocket signaling server (4 demo scenarios)
|
|
76
|
+
├── bench/ 500 MB DataChannel throughput benchmark
|
|
77
|
+
└── demo-flutter/ Flutter macOS client (flutter_webrtc)
|
|
78
|
+
|
|
79
|
+
features/ Cucumber BDD acceptance tests (living specification)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Each package is independently importable. `@ts-rtc/webrtc` is the only package most consumers need.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Quickstart
|
|
87
|
+
|
|
88
|
+
### Prerequisites
|
|
89
|
+
|
|
90
|
+
- Node.js 18+
|
|
91
|
+
- pnpm
|
|
92
|
+
|
|
93
|
+
### Install
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git clone https://github.com/your-org/ts-rtc.git
|
|
97
|
+
cd ts-rtc
|
|
98
|
+
pnpm install
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Build
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pnpm build # compile all packages to dist/
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Run tests
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pnpm test # Vitest unit tests across all packages
|
|
111
|
+
pnpm test:bdd # Cucumber BDD acceptance tests
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Usage
|
|
117
|
+
|
|
118
|
+
### Minimal DataChannel (peer-to-peer in Node.js)
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { RTCPeerConnection } from '@ts-rtc/webrtc';
|
|
122
|
+
|
|
123
|
+
// ── Offerer ───────────────────────────────────────────────────────────────────
|
|
124
|
+
const pcA = new RTCPeerConnection({ iceServers: [] });
|
|
125
|
+
const dc = pcA.createDataChannel('chat');
|
|
126
|
+
|
|
127
|
+
dc.on('open', () => dc.send('Hello WebRTC!'));
|
|
128
|
+
dc.on('message', data => console.log('[A received]', data));
|
|
129
|
+
|
|
130
|
+
// ── Answerer ──────────────────────────────────────────────────────────────────
|
|
131
|
+
const pcB = new RTCPeerConnection({ iceServers: [] });
|
|
132
|
+
|
|
133
|
+
pcB.on('datachannel', channel => {
|
|
134
|
+
channel.on('message', data => {
|
|
135
|
+
console.log('[B received]', data);
|
|
136
|
+
channel.send('Hello back!');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── Trickle ICE ───────────────────────────────────────────────────────────────
|
|
141
|
+
pcA.on('icecandidate', c => c && pcB.addIceCandidate(c));
|
|
142
|
+
pcB.on('icecandidate', c => c && pcA.addIceCandidate(c));
|
|
143
|
+
|
|
144
|
+
// ── SDP exchange ──────────────────────────────────────────────────────────────
|
|
145
|
+
const offer = await pcA.createOffer();
|
|
146
|
+
await pcA.setLocalDescription(offer);
|
|
147
|
+
await pcB.setRemoteDescription(offer);
|
|
148
|
+
|
|
149
|
+
const answer = await pcB.createAnswer();
|
|
150
|
+
await pcB.setLocalDescription(answer);
|
|
151
|
+
await pcA.setRemoteDescription(answer);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### With a STUN server
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const pc = new RTCPeerConnection({
|
|
158
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Binary data
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const buf = crypto.randomBytes(65536);
|
|
166
|
+
dc.on('open', () => dc.send(buf));
|
|
167
|
+
|
|
168
|
+
remoteChannel.on('message', (data: Buffer) => {
|
|
169
|
+
console.log('received', data.byteLength, 'bytes');
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Multiple concurrent channels
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
const ctrl = pcA.createDataChannel('control', { ordered: true });
|
|
177
|
+
const bulk = pcA.createDataChannel('bulk', { ordered: false });
|
|
178
|
+
const log = pcA.createDataChannel('log', { maxRetransmits: 0 });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Pre-negotiated channel (no DCEP round-trip)
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// Both peers must call this with the same id
|
|
185
|
+
const chA = pcA.createDataChannel('secure', { negotiated: true, id: 5 });
|
|
186
|
+
const chB = pcB.createDataChannel('secure', { negotiated: true, id: 5 });
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Backpressure-aware large transfers
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const CHUNK = 1168; // one SCTP DATA payload (fits within PMTU)
|
|
193
|
+
const HIGH = 4 * 1024 * 1024;
|
|
194
|
+
const LOW = 2 * 1024 * 1024;
|
|
195
|
+
|
|
196
|
+
dc.bufferedAmountLowThreshold = LOW;
|
|
197
|
+
|
|
198
|
+
function pump(data: Buffer, offset = 0) {
|
|
199
|
+
while (offset < data.length) {
|
|
200
|
+
if (dc.bufferedAmount > HIGH) {
|
|
201
|
+
dc.once('bufferedamountlow', () => pump(data, offset));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
dc.send(data.subarray(offset, offset + CHUNK));
|
|
205
|
+
offset += CHUNK;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
dc.on('open', () => pump(largeBuffer));
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Connection state monitoring
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
pc.on('connectionstatechange', () => {
|
|
216
|
+
console.log('connection:', pc.connectionState);
|
|
217
|
+
// 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed'
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
pc.on('iceconnectionstatechange', () => {
|
|
221
|
+
console.log('ICE:', pc.iceConnectionState);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
pc.on('icegatheringstatechange', () => {
|
|
225
|
+
console.log('gathering:', pc.iceGatheringState);
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Stats
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
const stats = await pc.getStats();
|
|
233
|
+
for (const [, entry] of stats) {
|
|
234
|
+
if (entry.type === 'candidate-pair' && entry.nominated) {
|
|
235
|
+
console.log('RTT:', entry.currentRoundTripTime);
|
|
236
|
+
console.log('bytes sent:', entry.bytesSent);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Graceful close
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
await dc.close();
|
|
245
|
+
pc.close();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## RTCPeerConnection API Reference
|
|
251
|
+
|
|
252
|
+
### Constructor
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
new RTCPeerConnection(config?: RTCConfiguration)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| Option | Type | Default |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| `iceServers` | `RTCIceServer[]` | `[{ urls: 'stun:stun.l.google.com:19302' }]` |
|
|
261
|
+
| `iceTransportPolicy` | `'all' \| 'relay'` | `'all'` |
|
|
262
|
+
| `bundlePolicy` | `'max-bundle' \| 'balanced' \| 'max-compat'` | `'max-bundle'` |
|
|
263
|
+
| `rtcpMuxPolicy` | `'require'` | `'require'` |
|
|
264
|
+
| `iceCandidatePoolSize` | `number` | `0` |
|
|
265
|
+
|
|
266
|
+
### Methods
|
|
267
|
+
|
|
268
|
+
| Method | Description |
|
|
269
|
+
|---|---|
|
|
270
|
+
| `createOffer()` | Generate an SDP offer |
|
|
271
|
+
| `createAnswer()` | Generate an SDP answer |
|
|
272
|
+
| `setLocalDescription(sdp)` | Apply local SDP, begin ICE gathering |
|
|
273
|
+
| `setRemoteDescription(sdp)` | Apply remote SDP, begin ICE connectivity checks |
|
|
274
|
+
| `addIceCandidate(candidate)` | Feed a trickled ICE candidate |
|
|
275
|
+
| `createDataChannel(label, init?)` | Create a DataChannel |
|
|
276
|
+
| `addTransceiver(kind, init?)` | Add an RTP transceiver |
|
|
277
|
+
| `getTransceivers()` | List all transceivers |
|
|
278
|
+
| `getSenders()` | List RTP senders |
|
|
279
|
+
| `getReceivers()` | List RTP receivers |
|
|
280
|
+
| `getStats()` | Retrieve `RTCStatsReport` |
|
|
281
|
+
| `restartIce()` | Trigger ICE restart |
|
|
282
|
+
| `close()` | Tear down the connection |
|
|
283
|
+
|
|
284
|
+
### Events
|
|
285
|
+
|
|
286
|
+
| Event | Payload | When |
|
|
287
|
+
|---|---|---|
|
|
288
|
+
| `icecandidate` | `RTCIceCandidateInit \| null` | New local ICE candidate; `null` = gathering complete |
|
|
289
|
+
| `icecandidateerror` | `{ errorCode, errorText }` | STUN server unreachable |
|
|
290
|
+
| `iceconnectionstatechange` | — | ICE connection state changed |
|
|
291
|
+
| `icegatheringstatechange` | — | ICE gathering state changed |
|
|
292
|
+
| `connectionstatechange` | — | Overall connection state changed |
|
|
293
|
+
| `signalingstatechange` | — | Signaling state changed |
|
|
294
|
+
| `negotiationneeded` | — | Re-negotiation required |
|
|
295
|
+
| `datachannel` | `RTCDataChannel` | Remote opened a DataChannel |
|
|
296
|
+
| `track` | `RTCTrackEvent` | Remote RTP track received |
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## RTCDataChannel API Reference
|
|
301
|
+
|
|
302
|
+
### Properties
|
|
303
|
+
|
|
304
|
+
| Property | Type | Description |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| `label` | `string` | Channel name |
|
|
307
|
+
| `readyState` | `'connecting' \| 'open' \| 'closing' \| 'closed'` | Current state |
|
|
308
|
+
| `ordered` | `boolean` | Reliable ordering |
|
|
309
|
+
| `maxPacketLifeTime` | `number \| null` | Partial reliability (ms) |
|
|
310
|
+
| `maxRetransmits` | `number \| null` | Partial reliability (count) |
|
|
311
|
+
| `protocol` | `string` | Sub-protocol |
|
|
312
|
+
| `negotiated` | `boolean` | Pre-negotiated (no DCEP) |
|
|
313
|
+
| `id` | `number` | SCTP stream ID |
|
|
314
|
+
| `bufferedAmount` | `number` | Bytes queued in send buffer |
|
|
315
|
+
| `bufferedAmountLowThreshold` | `number` | Threshold for `bufferedamountlow` |
|
|
316
|
+
| `binaryType` | `'arraybuffer'` | Binary message format |
|
|
317
|
+
|
|
318
|
+
### Methods
|
|
319
|
+
|
|
320
|
+
| Method | Description |
|
|
321
|
+
|---|---|
|
|
322
|
+
| `send(data)` | Send `string \| Buffer \| ArrayBuffer \| ArrayBufferView` |
|
|
323
|
+
| `close()` | Close the channel |
|
|
324
|
+
|
|
325
|
+
### Events
|
|
326
|
+
|
|
327
|
+
| Event | Payload | When |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| `open` | — | Channel ready to send |
|
|
330
|
+
| `message` | `string \| Buffer` | Message received |
|
|
331
|
+
| `close` | — | Channel closed |
|
|
332
|
+
| `closing` | — | Close initiated |
|
|
333
|
+
| `error` | `Error` | Channel error |
|
|
334
|
+
| `bufferedamountlow` | — | Buffered amount crossed threshold |
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Lower-Level Package APIs
|
|
339
|
+
|
|
340
|
+
Each protocol layer is independently usable for specialized use-cases.
|
|
341
|
+
|
|
342
|
+
### `@ts-rtc/ice` — ICE Agent
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { IceAgent } from '@ts-rtc/ice';
|
|
346
|
+
|
|
347
|
+
const agent = new IceAgent({ role: 'controlling', iceServers: [] });
|
|
348
|
+
await agent.gather();
|
|
349
|
+
agent.setRemoteParameters({ usernameFragment: '…', password: '…' });
|
|
350
|
+
agent.addRemoteCandidate(candidate);
|
|
351
|
+
await agent.connect();
|
|
352
|
+
agent.send(Buffer.from('data'));
|
|
353
|
+
agent.on('data', (buf) => console.log(buf));
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### `@ts-rtc/dtls` — DTLS 1.2 Transport
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { DtlsTransport } from '@ts-rtc/dtls';
|
|
360
|
+
|
|
361
|
+
const dtls = new DtlsTransport(iceTransport, {
|
|
362
|
+
role: 'client', // or 'server'
|
|
363
|
+
remoteFingerprint: { algorithm: 'sha-256', value: '…' },
|
|
364
|
+
});
|
|
365
|
+
await dtls.start();
|
|
366
|
+
dtls.on('connected', () => {
|
|
367
|
+
const keys = dtls.getSrtpKeyingMaterial(); // { clientKey, serverKey, clientSalt, serverSalt }
|
|
368
|
+
});
|
|
369
|
+
dtls.send(Buffer.from('app data'));
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### `@ts-rtc/sctp` — SCTP Association
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { SctpAssociation } from '@ts-rtc/sctp';
|
|
376
|
+
|
|
377
|
+
const sctp = new SctpAssociation(dtlsTransport, { role: 'client', port: 5000 });
|
|
378
|
+
await sctp.connect();
|
|
379
|
+
const channel = await sctp.createDataChannel('chat');
|
|
380
|
+
channel.send('hello');
|
|
381
|
+
sctp.on('datachannel', (ch) => ch.on('message', console.log));
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### `@ts-rtc/srtp` — SRTP Protect / Unprotect
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import { createSrtpContext, srtpProtect, srtpUnprotect } from '@ts-rtc/srtp';
|
|
388
|
+
import { ProtectionProfile } from '@ts-rtc/srtp';
|
|
389
|
+
|
|
390
|
+
const ctx = createSrtpContext(ProtectionProfile.AES_128_CM_HMAC_SHA1_80, keyingMaterial);
|
|
391
|
+
const protected_ = srtpProtect(ctx, rtpPacket);
|
|
392
|
+
const unprotected = srtpUnprotect(ctx, protected_);
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### `@ts-rtc/stun` — STUN Codec
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
import { encodeMessage, decodeMessage, createBindingRequest } from '@ts-rtc/stun';
|
|
399
|
+
|
|
400
|
+
const req = createBindingRequest({ username: 'user:pass', priority: 12345 });
|
|
401
|
+
const buf = encodeMessage(req, 'password');
|
|
402
|
+
const msg = decodeMessage(buf);
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### `@ts-rtc/sdp` — SDP Parser / Serializer
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
import { parse, serialize, parseCandidate } from '@ts-rtc/sdp';
|
|
409
|
+
|
|
410
|
+
const session = parse(sdpString);
|
|
411
|
+
const text = serialize(session);
|
|
412
|
+
const cand = parseCandidate('candidate:…');
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### `@ts-rtc/rtp` — RTP / RTCP Codec
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import { encodeRtp, decodeRtp, encodeRtcpSr, decodeRtcp } from '@ts-rtc/rtp';
|
|
419
|
+
|
|
420
|
+
const packet = encodeRtp({ payloadType: 96, sequenceNumber: 1, timestamp: 0, ssrc: 42, payload });
|
|
421
|
+
const { header, payload } = decodeRtp(packet);
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Throughput Benchmark
|
|
427
|
+
|
|
428
|
+
Measures raw DataChannel throughput on a Node.js loopback — no network, pure protocol stack cost.
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
cd apps/bench
|
|
432
|
+
../../node_modules/.bin/tsx bench.ts
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**What it tests:**
|
|
436
|
+
- Two isolated Node.js processes (`sender` and `receiver`) connected via IPC-bridged signaling
|
|
437
|
+
- 500 MB binary transfer in 1168-byte chunks (matches SCTP DATA payload size for a 1200-byte PMTU)
|
|
438
|
+
- Backpressure via `bufferedAmountLowThreshold` (high-watermark 4 MB, low-watermark 2 MB)
|
|
439
|
+
- SHA-256 end-to-end integrity verification — the benchmark fails if a single byte is wrong
|
|
440
|
+
|
|
441
|
+
**Sample output:**
|
|
442
|
+
```
|
|
443
|
+
════════════════════════════════════════════════════════════
|
|
444
|
+
ts-rtc 500MB DataChannel Throughput Benchmark
|
|
445
|
+
Path: Node.js loopback (127.0.0.1)
|
|
446
|
+
════════════════════════════════════════════════════════════
|
|
447
|
+
Benchmark complete
|
|
448
|
+
SHA-256 verification: ✅ passed
|
|
449
|
+
Transfer time: 8.3 s
|
|
450
|
+
Average speed: 60.24 MB/s
|
|
451
|
+
Total wall time: 9.1 s
|
|
452
|
+
════════════════════════════════════════════════════════════
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Test Suite
|
|
458
|
+
|
|
459
|
+
### Unit tests — Vitest
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
pnpm test
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
| Package | Test file | Key coverage |
|
|
466
|
+
|---|---|---|
|
|
467
|
+
| `webrtc` | `webrtc.test.ts` (604 lines) | RTCPeerConnection lifecycle, SDP factory, DTLS role negotiation, full ICE+DTLS+SCTP loopback |
|
|
468
|
+
| `ice` | `ice.test.ts` (555 lines) | Candidate priority/foundation math, pair formation, loopback connectivity, restart, tier classification |
|
|
469
|
+
| `dtls` | `dtls.test.ts` (738 lines) | Record codec, handshake messages, PRF vectors, self-signed cert, AES-GCM, full loopback, both-client deadlock regression |
|
|
470
|
+
| `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 |
|
|
471
|
+
| `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 |
|
|
472
|
+
| `rtp` | `rtp.test.ts` (570 lines) | RTP encode/decode, CSRC, header extensions, all RTCP types, compound packets, sequence wrap, NTP conversion |
|
|
473
|
+
| `sdp` | `sdp.test.ts` (827 lines) | Chrome offer/answer parsing, round-trip fidelity, all candidate types, fingerprint, directions, SSRC groups, extmap |
|
|
474
|
+
| `stun` | `stun.test.ts` (569 lines) | All attribute types, XOR-MAPPED-ADDRESS (IPv4 + IPv6), MESSAGE-INTEGRITY (correct/wrong/tampered), FINGERPRINT, ICE attributes |
|
|
475
|
+
|
|
476
|
+
**Total: ~4,900 lines of unit tests across 9 test files.**
|
|
477
|
+
|
|
478
|
+
### BDD acceptance tests — Cucumber.js
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
pnpm test:bdd
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
29 scenarios across 5 feature files:
|
|
485
|
+
|
|
486
|
+
| Feature file | Scenarios | What it covers |
|
|
487
|
+
|---|---|---|
|
|
488
|
+
| `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** |
|
|
489
|
+
| `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** |
|
|
490
|
+
| `ice/ice-connectivity.feature` | 2 | ICE gathering (valid candidates), **ICE loopback connectivity** |
|
|
491
|
+
| `dtls/dtls-handshake.feature` | 2 | **DTLS loopback handshake**, matching SRTP keying material, app data exchange |
|
|
492
|
+
| `sctp/sctp-channels.feature` | 4 | SCTP handshake, DCEP open, **65 KiB binary transfer**, **4 MiB binary transfer** |
|
|
493
|
+
|
|
494
|
+
Reports are written to `reports/cucumber-report.html`.
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Other Commands
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
pnpm typecheck # TypeScript strict-mode check across all packages (no emit)
|
|
502
|
+
pnpm lint # ESLint 9 + @typescript-eslint
|
|
503
|
+
pnpm clean # Remove all dist/ directories
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## TypeScript Configuration
|
|
509
|
+
|
|
510
|
+
All packages share `tsconfig.base.json`:
|
|
511
|
+
|
|
512
|
+
```json
|
|
513
|
+
{
|
|
514
|
+
"target": "ES2022",
|
|
515
|
+
"module": "NodeNext",
|
|
516
|
+
"strict": true,
|
|
517
|
+
"exactOptionalPropertyTypes": true,
|
|
518
|
+
"noUncheckedIndexedAccess": true,
|
|
519
|
+
"noImplicitOverride": true
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
`exactOptionalPropertyTypes` and `noUncheckedIndexedAccess` are enabled intentionally — they catch protocol-level bugs at compile time that strict mode alone misses.
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Monorepo Layout
|
|
528
|
+
|
|
529
|
+
```
|
|
530
|
+
ts-rtc/
|
|
531
|
+
├── packages/ # Protocol stack (each independently publishable)
|
|
532
|
+
├── apps/ # Demo and benchmark applications
|
|
533
|
+
├── features/ # Cucumber BDD specs + step definitions
|
|
534
|
+
├── package.json # pnpm workspace root
|
|
535
|
+
├── pnpm-workspace.yaml
|
|
536
|
+
├── tsconfig.base.json # Shared compiler options
|
|
537
|
+
└── cucumber.yaml # BDD runner config
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## Design Principles
|
|
543
|
+
|
|
544
|
+
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.
|
|
545
|
+
|
|
546
|
+
2. **RFC first.** Every algorithm includes inline RFC section references. If behavior diverges from the spec, it is a bug.
|
|
547
|
+
|
|
548
|
+
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.
|
|
549
|
+
|
|
550
|
+
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.
|
|
551
|
+
|
|
552
|
+
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.
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## License
|
|
557
|
+
|
|
558
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentdance/node-webrtc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Production-grade WebRTC in pure TypeScript — RTCPeerConnection, DataChannel, ICE, DTLS, SCTP, SRTP. Zero native bindings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"webrtc",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
|
+
"README.md",
|
|
40
41
|
"dist",
|
|
41
42
|
"src"
|
|
42
43
|
],
|
|
@@ -46,13 +47,13 @@
|
|
|
46
47
|
"registry": "https://registry.npmjs.org/"
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
|
-
"@agentdance/node-webrtc-sdp": "1.0.
|
|
50
|
-
"@agentdance/node-webrtc-ice": "1.0.
|
|
51
|
-
"@agentdance/node-webrtc-dtls": "1.0.
|
|
52
|
-
"@agentdance/node-webrtc-srtp": "1.0.
|
|
53
|
-
"@agentdance/node-webrtc-rtp": "1.0.
|
|
54
|
-
"@agentdance/node-webrtc-sctp": "1.0.
|
|
55
|
-
"@agentdance/node-webrtc-stun": "1.0.
|
|
50
|
+
"@agentdance/node-webrtc-sdp": "1.0.1",
|
|
51
|
+
"@agentdance/node-webrtc-ice": "1.0.1",
|
|
52
|
+
"@agentdance/node-webrtc-dtls": "1.0.1",
|
|
53
|
+
"@agentdance/node-webrtc-srtp": "1.0.1",
|
|
54
|
+
"@agentdance/node-webrtc-rtp": "1.0.1",
|
|
55
|
+
"@agentdance/node-webrtc-sctp": "1.0.1",
|
|
56
|
+
"@agentdance/node-webrtc-stun": "1.0.1"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"typescript": "*",
|