@trystero-p2p/torrent 0.26.0 β†’ 0.27.0

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 CHANGED
@@ -1,100 +1,33 @@
1
1
  # ✨🀝✨ Trystero
2
2
 
3
- **Build instant multiplayer web apps, no server required**
3
+ **Instant peer-to-peer multiplayer for the web β€” no server required.**
4
4
 
5
5
  πŸ‘‰ **[Try it live on trystero.dev](https://trystero.dev)** πŸ‘ˆ
6
6
 
7
- Trystero makes browsers discover each other and communicate directly. No
8
- accounts. No deploying infrastructure. Just import and connect.
7
+ Making your app work with multiple users usually means a detour into setting up
8
+ servers, auth, billing, databases, etc. β€” all so two browsers can say hello.
9
9
 
10
- Peers can connect via 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT, ⚑️ Supabase, πŸ”₯Firebase,
11
- πŸͺ IPFS, or a πŸ”Œ self-hosted WebSocket relay – all using the same API.
12
-
13
- Besides making peer matching automatic, Trystero offers some nice abstractions
14
- on top of WebRTC:
15
-
16
- - πŸ‘‚πŸ“£ Rooms / broadcasting
17
- - πŸ”’πŸ“© Automatic serialization / deserialization of data
18
- - πŸŽ₯🏷 Attach metadata to binary data and media streams
19
- - βœ‚οΈβ³ Automatic chunking and throttling of large data
20
- - ⏱🀞 Progress events and promises for data transfers
21
- - πŸ”πŸ“ Session data encryption
22
- - 🏭⚑ Can run peers server-side on Node, Bun, and Deno
23
- - βš›οΈπŸͺ React hooks
24
-
25
- You can see what people are building with Trystero
26
- [here](https://github.com/jeremyckahn/awesome-trystero).
27
-
28
- ---
29
-
30
- ## Contents
31
-
32
- - [How it works](#how-it-works)
33
- - [Get started](#get-started)
34
- - [Listen for events](#listen-for-events)
35
- - [Broadcast events](#broadcast-events)
36
- - [Audio and video](#audio-and-video)
37
- - [Advanced](#advanced)
38
- - [Binary metadata](#binary-metadata)
39
- - [Receiving files](#receiving-files)
40
- - [Action promises](#action-promises)
41
- - [Progress updates](#progress-updates)
42
- - [Encryption](#encryption)
43
- - [React hooks](#react-hooks)
44
- - [Troubleshooting connection issues](#troubleshooting-connection-issues)
45
- - [Running server-side (Node, Bun, Deno)](#running-server-side-node-bun-deno)
46
- - [Self-hosted WebSocket relay](#self-hosted-websocket-relay)
47
- - [Write your own strategy](#write-your-own-strategy)
48
- - [Supabase setup](#supabase-setup)
49
- - [Firebase setup](#firebase-setup)
50
- - [API](#api)
51
- - [`joinRoom(config, roomId, [callbacks])`](#joinroomconfig-roomid-callbacks)
52
- - [`selfId`](#selfid)
53
- - [`getRelaySockets()`](#getrelaysockets)
54
- - [`pauseRelayReconnection()`](#pauserelayreconnection)
55
- - [`resumeRelayReconnection()`](#resumerelayreconnection)
56
- - [Which strategy should I choose?](#which-strategy-should-i-choose)
10
+ **Trystero skips the backend entirely.** Import the library, join a room by
11
+ name, and browsers discover each other automatically and connect directly over
12
+ fast, end-to-end encrypted WebRTC. Out of the box, you get verified user
13
+ identities, synced user and room state, host permissions, RPC, chunked file
14
+ transfers, and live audio/video. The web was meant to be decentralized and
15
+ Trystero makes it easy to build apps where data flows without middlemen.
57
16
 
58
17
  ---
59
18
 
60
- ## How it works
61
-
62
- πŸ‘‰ **If you just want to try out Trystero, you can skip this explainer and
63
- [jump into using it](#get-started).**
64
-
65
- To establish a direct peer-to-peer connection with WebRTC, a signalling channel
66
- is needed to exchange peer information
67
- ([SDP](https://en.wikipedia.org/wiki/Session_Description_Protocol)). Typically
68
- this involves running your own matchmaking server but Trystero abstracts this
69
- away for you and offers multiple strategies for connecting peers (currently
70
- BitTorrent, Nostr, MQTT, Supabase, Firebase, IPFS, and self-hosted WebSocket
71
- relay).
19
+ ## Quickstart
72
20
 
73
- The important point to remember is this:
74
-
75
- > πŸ”’
76
- >
77
- > Beyond peer discovery, your app's data never touches the strategy medium and
78
- > is sent directly peer-to-peer and end-to-end encrypted between users.
79
- >
80
- > πŸ‘†
81
-
82
- You can [compare strategies here](#which-strategy-should-i-choose).
83
-
84
- ## Get started
85
-
86
- Install Trystero with your preferred package manager, then import it in your
87
- code:
21
+ Install Trystero from npm:
88
22
 
89
23
  ```sh
90
24
  npm i trystero
91
25
  ```
92
26
 
93
- ```js
94
- import {joinRoom} from 'trystero'
95
- ```
96
-
97
- No package manager? You can also use a CDN:
27
+ <details>
28
+ <summary>
29
+ (Or skip the build step and import from a script tag)
30
+ </summary>
98
31
 
99
32
  ```html
100
33
  <script type="module">
@@ -102,1066 +35,337 @@ No package manager? You can also use a CDN:
102
35
  </script>
103
36
  ```
104
37
 
105
- The default Trystero package runs on the Nostr network, but you can swap in any
106
- other stategy by changing which package you import:
107
-
108
- ```js
109
- import {joinRoom} from '@trystero-p2p/mqtt'
110
- // or
111
- import {joinRoom} from '@trystero-p2p/torrent'
112
- // or
113
- import {joinRoom} from '@trystero-p2p/supabase'
114
- // or
115
- import {joinRoom} from '@trystero-p2p/firebase'
116
- // or
117
- import {joinRoom} from '@trystero-p2p/ipfs'
118
- // or
119
- import {joinRoom} from '@trystero-p2p/ws-relay'
120
- ```
38
+ </details>
121
39
 
122
- Next, join the user to a room with an ID:
40
+ Join a room with a unique app ID and room name and start syncing state. Here's a
41
+ complete multiplayer cursor canvas in a dozen lines:
123
42
 
124
43
  ```js
125
- const config = {appId: 'san_narciso_3d'}
126
- const room = joinRoom(config, 'yoyodyne')
127
- ```
128
-
129
- The first argument is a configuration object that requires an `appId`. This
130
- should be a completely unique identifier for your appΒΉ. The second argument is
131
- the room ID.
132
-
133
- > Why rooms? Browsers can only handle a limited amount of WebRTC connections at
134
- > a time so it's recommended to design your app such that users are divided into
135
- > groups (or rooms, or namespaces, or channels... whatever you'd like to call
136
- > them).
44
+ import {joinRoom} from 'trystero'
137
45
 
138
- ΒΉ When using Firebase, `appId` should be your `databaseURL` and when using
139
- Supabase, it should be your project URL.
46
+ // No API keys, accounts, or backend setup. Just pick a globally unique appId
47
+ // (e.g. your app's domain or 'username/app-name') and a room name
48
+ const room = joinRoom({appId: 'pixel-party.example.com'}, 'studio-49')
140
49
 
141
- ## Listen for events
50
+ // Give every user their own signed state slot
51
+ const cursor = room.makeUserState('cursor', {x: 0, y: 0})
142
52
 
143
- Listen for peers joining the room:
53
+ window.addEventListener('pointermove', e =>
54
+ cursor.set({x: e.clientX, y: e.clientY})
55
+ )
144
56
 
145
- ```js
146
- room.onPeerJoin = peerId => console.log(`${peerId} joined`)
57
+ cursor.onChange((pos, {userId, self, removed}) => {
58
+ if (self) {
59
+ return
60
+ }
61
+ if (removed) {
62
+ removeCursor(userId)
63
+ } else {
64
+ drawCursor(userId, pos)
65
+ }
66
+ })
147
67
  ```
148
68
 
149
- Listen for peers leaving the room:
69
+ Open your page in two browsers anywhere in the world and watch the cursors move.
150
70
 
151
- ```js
152
- room.onPeerLeave = peerId => console.log(`${peerId} left`)
153
- ```
71
+ > **Local testing tip:** Tabs in the same browser profile share your `userId`
72
+ > automatically. Open a Private/Incognito window (or pass `identity: 'none'` in
73
+ > the room config) when simulating a second person on one machine.
154
74
 
155
- Listen for peers sending their audio/video streams:
75
+ ---
156
76
 
157
- ```js
158
- room.onPeerStream = (stream, peerId) =>
159
- (peerElements[peerId].video.srcObject = stream)
160
- ```
77
+ ## What you can build with it
161
78
 
162
- To unsubscribe from events, leave the room:
79
+ ### 1. Synced user & room state
163
80
 
164
- ```js
165
- room.leave()
166
- ```
81
+ Most multiplayer apps need two kinds of live state: **per-user state** that each
82
+ participant owns (names, avatars, ready flags, cursors) and **shared room
83
+ state** for the group (a game board, round timer, or watch-party queue).
167
84
 
168
- You can access the local user's peer ID by importing `selfId` like so:
85
+ Trystero builds both right onto `room`. Updates apply immediately locally,
86
+ coalesce rapid writes on the wire, verify who wrote them, and sync automatically
87
+ to late joiners β€” even if the person who last updated shared state has already
88
+ closed their laptop.
169
89
 
170
90
  ```js
171
- import {selfId} from 'trystero'
172
-
173
- console.log(`my peer ID is ${selfId}`)
174
- ```
91
+ // Each player controls only their own signed slot β€” nobody can overwrite yours
92
+ const profile = room.makeUserState('profile', {name: 'Anon', ready: false})
93
+ profile.set({name: 'Oedipa', ready: true})
175
94
 
176
- ## Broadcast events
95
+ // Snapshot of everyone currently in the room: { [userId]: { name, ready } }
96
+ console.log(profile.getAll())
177
97
 
178
- Send peers your video stream:
98
+ // Shared state for the whole room, synced automatically to latecomers
99
+ const track = room.makeState('track', {url: 'chill-beat.mp3', paused: true})
179
100
 
180
- ```js
181
- const stream = await navigator.mediaDevices.getUserMedia({
182
- audio: true,
183
- video: true
101
+ track.onChange((next, {userId}) => {
102
+ syncAudioPlayer(next)
103
+ console.log(`Updated by ${userId}`)
184
104
  })
185
- room.addStream(stream)
186
- ```
187
105
 
188
- Send and subscribe to custom peer-to-peer actions:
189
-
190
- ```js
191
- const drink = room.makeAction('drink')
106
+ await track.set(prev => ({...prev, paused: false}))
107
+ ```
192
108
 
193
- // buy drink for a friend
194
- drink.send({drink: 'negroni', withIce: true}, {target: friendId})
109
+ Using React? Both `makeUserState` and `makeState` plug straight into
110
+ `useSyncExternalStore` β€” no `useEffect` glue or manual join/leave maps:
195
111
 
196
- // buy round for the house
197
- drink.send({drink: 'mezcal', withIce: false})
112
+ ```jsx
113
+ import {useSyncExternalStore} from 'react'
198
114
 
199
- // listen for drinks sent to you
200
- drink.onMessage = (data, {peerId}) =>
201
- console.log(
202
- `got a ${data.drink} with${data.withIce ? '' : 'out'} ice from ${peerId}`
203
- )
115
+ function Lobby() {
116
+ const players = useSyncExternalStore(profile.onChange, profile.getAll)
117
+ const nowPlaying = useSyncExternalStore(track.onChange, track.get)
118
+ // render directly from `players` and `nowPlaying`
119
+ }
204
120
  ```
205
121
 
206
- Actions can also use request/response semantics:
122
+ πŸ‘‰ **[Shared & Per-User State guide β†’](https://github.com/dmotz/trystero/blob/main/docs/state.md)**
207
123
 
208
- ```js
209
- const isEven = room.makeAction('is-even', {
210
- kind: 'request',
211
- onRequest: n => n % 2 === 0
212
- })
124
+ ---
213
125
 
214
- const result = await isEven.request(42, {
215
- target: friendId,
216
- timeoutMs: 1000
217
- })
218
- ```
126
+ ### 2. Unforgeable user identity, passwords & room caps
219
127
 
220
- To ask multiple peers at once, use `requestMany()`. It resolves with a
221
- peer-labeled result for every target, while `onResult` lets you react as each
222
- peer answers:
128
+ Every participant gets a persistent, cryptographically verified `userId` out of
129
+ the box β€” no login screens or auth servers required. If someone refreshes the
130
+ page or opens your app in two tabs at once, Trystero recognizes them as the same
131
+ user automatically. (Want strictly throwaway IDs or keys loaded from your own
132
+ backend? One config option switches modes.)
223
133
 
224
- ```js
225
- const availability = room.makeAction('availability', {
226
- kind: 'request',
227
- onRequest: ({date}) => calendar.isFree(date)
228
- })
134
+ Combine `password` and `maxUsers` to spin up private, seat-capped matches like a
135
+ 2-player chess table or a 4-player co-op lobby:
229
136
 
230
- const results = await availability.requestMany(
231
- {date: '2026-05-04'},
137
+ ```js
138
+ const room = joinRoom(
232
139
  {
233
- targets: teammateIds,
234
- timeoutMs: 1000,
235
- onResult: result => {
236
- if (result.status === 'fulfilled') {
237
- updateAvailabilityBadge(result.peerId, result.value)
238
- }
239
- }
240
- }
140
+ appId: 'chess.example.com',
141
+ password: 'knight-takes-f7-secret', // encrypts discovery so outsiders stay out
142
+ maxUsers: 2 // cap this match at 2 unique players (you + 1 opponent)
143
+ },
144
+ 'table-7'
241
145
  )
242
146
 
243
- const freePeers = results
244
- .filter(result => result.status === 'fulfilled' && result.value)
245
- .map(result => result.peerId)
246
- ```
247
-
248
- If you're using TypeScript, you can add a type hint to the action:
147
+ const {userId} = await room.getSelf()
249
148
 
250
- ```typescript
251
- type CursorMove = {x: number; y: number}
149
+ room.onUserJoin(({userId}) => console.log(`Opponent ${userId} sat down`))
150
+ room.onUserLeave(({userId}) => console.log(`Opponent ${userId} left`))
252
151
 
253
- const cursor = room.makeAction<CursorMove>('cursor-move')
152
+ room.onError(err => {
153
+ if (err.code === 'room-full' && Object.keys(room.getPeers()).length === 0) {
154
+ showBanner('Sorry, both seats at this table are taken!')
155
+ }
156
+ })
254
157
  ```
255
158
 
256
- You can also use actions to send binary data, like images:
159
+ πŸ‘‰ **[Identity, Presence & Room Access guide β†’](https://github.com/dmotz/trystero/blob/main/docs/identity.md)**
257
160
 
258
- ```js
259
- const pic = room.makeAction('pic')
161
+ ---
260
162
 
261
- // blobs are automatically handled, as are any form of TypedArray
262
- canvas.toBlob(blob => pic.send(blob))
163
+ ### 3. Serverless hosts, custom roles & moderation
263
164
 
264
- // binary data is received as raw ArrayBuffers so your handling code should
265
- // interpret it in a way that makes sense
266
- pic.onMessage = (data, {peerId}) =>
267
- (imgs[peerId].src = URL.createObjectURL(new Blob([data])))
268
- ```
165
+ In a typical P2P app, everyone is equal β€” which gets awkward the moment someone
166
+ opens DevTools in your game and awards themselves a million points.
269
167
 
270
- Let's say we want users to be able to name themselves:
168
+ Import the tree-shakeable `roles()` plugin to create rooms with mathematically
169
+ verified admins, custom roles (like `'dj'` or `'moderator'`), authoritative
170
+ state that only authorized roles can edit, and peer banning β€” still with zero
171
+ backend:
271
172
 
272
173
  ```js
273
- const idsToNames = {}
274
- const name = room.makeAction('name')
174
+ import {joinRoom, roles} from 'trystero'
275
175
 
276
- // tell new peers your name when they connect
277
- room.onPeerJoin = peerId => name.send('Oedipa', {target: peerId})
176
+ // Host creates a room where their userId has verified admin authority
177
+ const room = joinRoom(
178
+ {appId: 'oedipa/friday-trivia', use: [roles({selfAsAdmin: true})]},
179
+ 'quiz-night'
180
+ )
278
181
 
279
- // listen for peers naming themselves
280
- name.onMessage = (value, {peerId}) => (idsToNames[peerId] = value)
182
+ // Share `roomId` (e.g. "quiz-night~9fX2kLm...") in an invite link;
183
+ // guests join with `use: [roles()]` and automatically verify the host
184
+ const {roomId} = await room.getSelf()
281
185
 
282
- // tell all peers at once when your name changes
283
- nameInput.addEventListener('change', e => name.send(e.target.value))
186
+ // With roles() enabled, makeState defaults to admin-only writes β€”
187
+ // everyone can read `game`, and peers reject forged edits automatically
188
+ const game = room.makeState('game', {round: 1, prompt: 'Ready?'})
189
+ await game.set({round: 2, prompt: 'Name the moons of Mars'})
284
190
 
285
- room.onPeerLeave = peerId =>
286
- console.log(`${idsToNames[peerId] || 'a weird stranger'} left`)
191
+ // Promote helpers or boot troublemakers
192
+ await room.grantRole(friendId, 'cohost')
193
+ await room.ban(grieferId, {reason: 'spoiling answers'})
287
194
  ```
288
195
 
289
- > Actions are smart and handle serialization and chunking for you behind the
290
- > scenes. This means you can send very large files and whatever data you send
291
- > will be received on the other side as the same type (a number as a number, a
292
- > string as a string, an object as an object, binary as binary, etc.). See
293
- > [receiving files](#receiving-files) for optional size limits and acceptance
294
- > callbacks.
295
-
296
- ## Audio and video
297
-
298
- Here's a simple example of how you could create an audio chatroom:
196
+ πŸ‘‰ **[Admins, Roles & Moderation guide β†’](https://github.com/dmotz/trystero/blob/main/docs/roles.md)**
299
197
 
300
- ```js
301
- // this object can store audio instances for later
302
- const peerAudios = {}
303
-
304
- // get a local audio stream from the microphone
305
- const selfStream = await navigator.mediaDevices.getUserMedia({
306
- audio: true,
307
- video: false
308
- })
309
-
310
- // send stream to peers currently in the room
311
- room.addStream(selfStream)
312
-
313
- // send stream to peers who join later
314
- room.onPeerJoin = peerId => room.addStream(selfStream, {target: peerId})
315
-
316
- // handle streams from other peers
317
- room.onPeerStream = (stream, peerId) => {
318
- // create an audio instance and set the incoming stream
319
- const audio = new Audio()
320
- audio.srcObject = stream
321
- audio.autoplay = true
198
+ ---
322
199
 
323
- // add the audio to peerAudios object if you want to address it for something
324
- // later (volume, etc.)
325
- peerAudios[peerId] = audio
326
- }
327
- ```
200
+ ### 4. Events, RPC & direct P2P file drop
328
201
 
329
- Doing the same with video is similar, just be sure to add incoming streams to
330
- video elements in the DOM:
202
+ For ephemeral events, request/response calls, and binary transfers, create typed
203
+ channels with `room.makeAction()`. Pass strings, JSON objects, `Blob`s, `File`s,
204
+ or `TypedArray`s β€” Trystero handles serialization, chunking, backpressure,
205
+ metadata headers, and transfer progress automatically, with zero cloud storage
206
+ bills:
331
207
 
332
208
  ```js
333
- const peerVideos = {}
334
- const videoContainer = document.getElementById('videos')
335
-
336
- room.onPeerStream = (stream, peerId) => {
337
- let video = peerVideos[peerId]
338
-
339
- // if this peer hasn't sent a stream before, create a video element
340
- if (!video) {
341
- video = document.createElement('video')
342
- video.autoplay = true
343
-
344
- // add video element to the DOM
345
- videoContainer.appendChild(video)
346
- }
347
-
348
- video.srcObject = stream
349
- peerVideos[peerId] = video
350
- }
351
- ```
352
-
353
- ## Advanced
209
+ const drop = room.makeAction('file-drop')
354
210
 
355
- ### Binary metadata
211
+ // Optionally add a predicate too accept based on max byteLength
212
+ drop.onReceive(({byteLength}) => byteLength <= 100 * 1024 ** 2)
356
213
 
357
- Let's say your app supports sending various types of files and you want to
358
- annotate the raw bytes being sent with metadata about how they should be
359
- interpreted. Instead of manually adding metadata bytes to the buffer you can
360
- simply pass a metadata argument in the sender action for your binary payload:
361
-
362
- ```js
363
- const file = room.makeAction('file')
214
+ // Listen for progress events to show a loading bar
215
+ drop.onReceiveProgress((pct, {metadata}) => showDownloadBar(metadata.name, pct))
364
216
 
365
- file.onMessage = (data, {peerId, metadata}) =>
366
- console.log(
367
- `got a file (${metadata.name}) from ${peerId} with type ${metadata.type}`,
368
- data
369
- )
217
+ // When the file is done being received, save it
218
+ drop.onMessage((bytes, {userId, metadata}) => saveFile(bytes, metadata.name))
370
219
 
371
- file.send(buffer, {
372
- metadata: {name: 'The CourierΚΌs Tragedy', type: 'application/pdf'}
220
+ // Send a file to the room with optional metadata
221
+ await drop.send(file, {
222
+ metadata: {name: file.name},
223
+ onProgress: pct => showUploadBar(pct)
373
224
  })
374
- ```
375
225
 
376
- ### Receiving files
226
+ // Create a request action where you ask a peer for something and they provide
227
+ // an answer
228
+ const roll = room.makeAction('roll-check', {kind: 'request'})
377
229
 
378
- Incoming data is accepted automatically. Use `onReceive` to accept or decline
379
- payloads based on their size, sender, or metadata:
230
+ // Respond to roll requests from other peers
231
+ roll.onRequest(() => Math.ceil(Math.random() * 20))
380
232
 
381
- ```js
382
- const file = room.makeAction('file', {
383
- onReceive: ({byteLength}) => byteLength <= 100 * 1024 ** 2,
384
- onMessage: (data, {metadata}) => saveFile(data, metadata.name)
385
- })
233
+ // Ask a specific friendId for their roll number
234
+ const result = await roll.request(null, {target: friendId, timeoutMs: 2000})
386
235
  ```
387
236
 
388
- Return `true` or `false`, or a promise of either. Large payloads wait for
389
- approval before sending their contents; declined small messages are discarded
390
- after arrival. You can also assign `file.onReceive` later or set it to `null` to
391
- restore automatic acceptance.
392
-
393
- `byteLength` is the payload size in bytes (UTF-8 for strings/JSON), excluding
394
- metadata. The context's `signal` aborts when the transfer ends; use it to
395
- dismiss pending approval UI.
237
+ πŸ‘‰ **[Actions, RPC & File Transfers guide β†’](https://github.com/dmotz/trystero/blob/main/docs/actions.md)**
396
238
 
397
- Each room defaults to a 256 MiB receive budget. Set `maxReceiveBytes` in the
398
- room config to allow larger files or reduce memory use. Concurrent transfers
399
- queue automatically when the budget is occupied. Received files must fit in
400
- memory.
401
-
402
- ### Action promises
403
-
404
- Sender promises resolve when local sending finishes; they do not confirm
405
- delivery or application handling. If a receiver declines a large transfer, the
406
- promise rejects with `error.kind === 'rejected'`. Use request actions when you
407
- need a response.
408
-
409
- ```js
410
- await file.send(amplePayload)
411
- console.log('done sending to all peers')
412
- ```
239
+ ---
413
240
 
414
- ### Progress updates
241
+ ### 5. Voice, video & screen sharing
415
242
 
416
- Action sender functions also take an optional callback function that will be
417
- continuously called as the transmission progresses. This can be used for showing
418
- a progress bar to the sender for large transfers. The callback is called with a
419
- percentage value between 0 and 1 and the receiving peer's ID:
243
+ Drop live audio, webcam video, screen captures, or Web Audio streams into any
244
+ room, and tag streams with `metadata` so peers can tell a facecam from a screen
245
+ share:
420
246
 
421
247
  ```js
422
- file.send(payload, {
423
- target: [peerIdA, peerIdB, peerIdC],
424
- metadata: {filename: 'paranoids.flac'},
425
- onProgress: (percent, {peerId}) => (loadingBars[peerId].value = percent)
248
+ const stream = await navigator.mediaDevices.getUserMedia({
249
+ audio: true,
250
+ video: true
426
251
  })
427
- ```
428
252
 
429
- Similarly you can listen for progress events as a receiver like this:
430
-
431
- ```js
432
- const file = room.makeAction('file')
253
+ // Send to everyone here now, plus anyone who joins later
254
+ await room.addStream(stream, {metadata: {type: 'webcam'}})
255
+ room.onUserJoin(({userId}) =>
256
+ room.addStream(stream, {target: userId, metadata: {type: 'webcam'}})
257
+ )
433
258
 
434
- file.onReceiveProgress = (percent, {peerId, metadata}) =>
435
- console.log(
436
- `${percent * 100}% done receiving ${metadata.filename} from ${peerId}`
437
- )
259
+ // Add a video when receiving a stream from someone else
260
+ room.onPeerStream((remoteStream, {userId, metadata}) =>
261
+ attachVideo(userId, remoteStream, metadata.type)
262
+ )
438
263
  ```
439
264
 
440
- Notice that any metadata is sent with progress events so you can show the
441
- receiving user that there is a transfer in progress with perhaps the name of the
442
- incoming file.
265
+ πŸ‘‰ **[Audio, Video & Screen Sharing guide β†’](https://github.com/dmotz/trystero/blob/main/docs/media.md)**
443
266
 
444
- Since a peer can send multiple transmissions in parallel, you can also use
445
- metadata to differentiate between them, e.g. by sending a unique ID.
267
+ ---
446
268
 
447
- ### Encryption
269
+ ### 6. Scale to large numbers of peers
448
270
 
449
- Once peers are connected to each other all of their communications are
450
- end-to-end encrypted. During the initial connection / discovery process, peers'
451
- [SDPs](https://en.wikipedia.org/wiki/Session_Description_Protocol) are sent via
452
- the chosen peering strategy medium. By default the SDP is encrypted using a key
453
- derived from your app ID and room ID to prevent plaintext session data from
454
- appearing in logs. This is fine for most use cases, however a relay strategy
455
- operator can reverse engineer the key using the room and app IDs. A more secure
456
- option is to pass a `password` parameter in the app configuration object which
457
- will be used to derive the encryption key:
271
+ Standard WebRTC connects every browser directly to every other browser, which is
272
+ ideal for smaller hangouts and matches. Building a 100-person collaborative
273
+ canvas or a crowded party lobby? Opt into the `scale()` plugin:
458
274
 
459
275
  ```js
460
- joinRoom({appId: 'kinneret', password: 'MuchoMaa$'}, 'w_a_s_t_e__v_i_p')
461
- ```
276
+ import {joinRoom, scale} from 'trystero'
462
277
 
463
- This is a shared secret that must be known ahead of time and the password must
464
- match for all peers in the room for them to be able to connect. An example use
465
- case might be a private chat room where users learn the password via external
466
- means.
467
-
468
- ### React hooks
469
-
470
- Trystero functions are idempotent so they already work out of the box as React
471
- hooks.
472
-
473
- Here's a simple example component where each peer syncs their favorite color to
474
- everyone else:
475
-
476
- ```jsx
477
- import {joinRoom} from 'trystero'
478
- import {useState} from 'react'
479
-
480
- const trysteroConfig = {appId: 'thurn-und-taxis'}
481
-
482
- export default function App({roomId}) {
483
- const room = joinRoom(trysteroConfig, roomId)
484
- const colorAction = room.makeAction('color')
485
- const [myColor, setMyColor] = useState('#c0ffee')
486
- const [peerColors, setPeerColors] = useState({})
487
-
488
- // whenever new peers join the room, send my color to them:
489
- room.onPeerJoin = peer => colorAction.send(myColor, {target: peer})
490
-
491
- // listen for peers sending their colors and update the state accordingly:
492
- colorAction.onMessage = (color, {peerId}) =>
493
- setPeerColors(peerColors => ({...peerColors, [peerId]: color}))
494
-
495
- const updateColor = e => {
496
- const {value} = e.target
497
-
498
- // when updating my own color, broadcast it to all peers:
499
- colorAction.send(value)
500
- setMyColor(value)
501
- }
502
-
503
- return (
504
- <>
505
- <h1>Trystero + React</h1>
506
-
507
- <h2>My color:</h2>
508
- <input type="color" value={myColor} onChange={updateColor} />
509
-
510
- <h2>Peer colors:</h2>
511
- <ul>
512
- {Object.entries(peerColors).map(([peerId, color]) => (
513
- <li key={peerId} style={{backgroundColor: color}}>
514
- {peerId}: {color}
515
- </li>
516
- ))}
517
- </ul>
518
- </>
519
- )
520
- }
278
+ const room = joinRoom({appId: 'canvas.example.com', use: [scale()]}, 'plaza')
521
279
  ```
522
280
 
523
- Astute readers may notice the above example is simple and doesn't consider if we
524
- want to change the component's room ID or unmount it. For those scenarios you
525
- can use this simple `useRoom()` hook that unsubscribes from room events
526
- accordingly:
281
+ Small rooms stay in a fast direct mesh with zero extra overhead. As more people
282
+ pile in, Trystero automatically forms a multi-hop gossip network so no single
283
+ browser gets overwhelmed β€” while your state, action, and media code stays
284
+ exactly the same.
527
285
 
528
- ```js
529
- import {joinRoom} from 'trystero'
530
- import {useEffect, useRef} from 'react'
531
-
532
- export const useRoom = (roomConfig, roomId) => {
533
- const roomRef = useRef(joinRoom(roomConfig, roomId))
534
- const lastRoomIdRef = useRef(roomId)
535
-
536
- useEffect(() => {
537
- if (roomId !== lastRoomIdRef.current) {
538
- roomRef.current.leave()
539
- roomRef.current = joinRoom(roomConfig, roomId)
540
- lastRoomIdRef.current = roomId
541
- }
286
+ πŸ‘‰ **[Scaling Large Rooms guide β†’](https://github.com/dmotz/trystero/blob/main/docs/scaling.md)**
542
287
 
543
- return () => roomRef.current.leave()
544
- }, [roomConfig, roomId])
288
+ ---
545
289
 
546
- return roomRef.current
547
- }
548
- ```
290
+ ### 7. Run headless peers in Node, Bun & Deno
549
291
 
550
- ### Troubleshooting connection issues
551
-
552
- WebRTC is powerful but some networks simply don't allow direct P2P connections
553
- using it. If you find that certain user pairings aren't working in Trystero,
554
- you're likely encountering an issue at the network provider level. To solve this
555
- you can configure a TURN server which will act as a proxy layer for peers that
556
- aren't able to connect directly to one another.
557
-
558
- 1. If you can, confirm that the issue is specific to particular network
559
- conditions (e.g. user with ISP X cannot connect to a user with ISP Y). If
560
- other user pairings are working (like those between two browsers on the same
561
- machine), this likely confirms that Trystero is working correctly.
562
- 2. Sign up for a TURN service or host your own. There are various hosted TURN
563
- services you can find online like
564
- [Cloudflare](https://developers.cloudflare.com/calls/turn/) (which offers a
565
- free tier with 1,000 GB traffic per month) or
566
- [Open Relay](https://www.metered.ca/stun-turn). You can also host an open
567
- source TURN server like [coturn](https://github.com/coturn/coturn),
568
- [Pion TURN](https://github.com/pion/turn),
569
- [Violet](https://github.com/paullouisageneau/violet), or
570
- [eturnal](https://github.com/processone/eturnal). Keep in mind data will only
571
- go through the TURN server for peers that can't directly connect and will
572
- still be end-to-end encrypted.
573
- 3. Once you have a TURN server, configure Trystero with it like this:
574
- ```js
575
- const room = joinRoom(
576
- {
577
- // ...your app config
578
- turnConfig: [
579
- {
580
- // single string or list of strings of URLs to access TURN server
581
- urls: ['turn:your-turn-server.ok:1979'],
582
- username: 'username',
583
- credential: 'password'
584
- }
585
- ]
586
- },
587
- 'roomId'
588
- )
589
- ```
590
-
591
- ### Running server-side (Node, Bun, Deno)
592
-
593
- Trystero works outside browsers too, like in Node, Bun, or Deno. Why would you
594
- want to run something that helps you avoid servers on a server? One reason is if
595
- you want an always-on peer which can be useful for remembering the last state of
596
- data, broadcasting it to new users. Another reason might be to run peers that
597
- are lighter weight and don't need a full browser running, like an embedded
598
- device or Raspberry Pi.
599
-
600
- Running server-side uses the same syntax as in the browser, but you need to
601
- import a polyfill for WebRTC support:
292
+ Trystero runs outside the browser too. Drop a lightweight peer onto a Raspberry
293
+ Pi, build a game bot, or run a silent `passive: true` peer on a tiny box to keep
294
+ shared room state warm even when every browser tab closes:
602
295
 
603
296
  ```js
604
297
  import {joinRoom} from 'trystero'
605
298
  import {RTCPeerConnection} from 'werift'
606
299
 
607
300
  const room = joinRoom(
608
- {appId: 'your-app-id', rtcPolyfill: RTCPeerConnection},
609
- 'your-room-name'
301
+ {appId: 'oedipa/synth-jam', rtcPolyfill: RTCPeerConnection, passive: true},
302
+ 'room-1'
610
303
  )
611
- ```
612
-
613
- ### Self-hosted WebSocket relay
614
-
615
- If you want a tiny relay that you control, use the WebSocket relay package.
616
- Start the relay on Node, Bun, or Deno:
617
-
618
- ```js
619
- import {createWsRelayServer} from '@trystero-p2p/ws-relay/server'
620
-
621
- createWsRelayServer({port: 8080})
622
- ```
623
-
624
- Then in the browser:
625
-
626
- ```js
627
- import {joinRoom} from '@trystero-p2p/ws-relay'
628
304
 
629
- const room = joinRoom(
630
- {
631
- appId: 'app-id',
632
- relayConfig: {
633
- urls: ['wss://localhost:8080']
634
- }
635
- },
636
- 'room-id'
637
- )
305
+ // Silently caches and syncs the latest pattern for newcomers
306
+ room.makeState('pattern', {bpm: 120, steps: []})
638
307
  ```
639
308
 
640
- The `relayConfig.urls` config option is required for this strategy because there
641
- are no public default servers. You can pass multiple.
642
-
643
- ### Write your own strategy
644
-
645
- If you want to provide your own signaling backend, you can build a custom
646
- strategy with `createTopicStrategy`. This is the recommended helper for simple
647
- pub/sub relays because Trystero handles room lifecycle details like passive
648
- mode, announce scheduling, and peer-specific signaling topics.
649
-
650
- The example below assumes a WebSocket relay that does simple pub/sub routing:
309
+ πŸ‘‰ **[Running in Node, Bun & Deno guide β†’](https://github.com/dmotz/trystero/blob/main/docs/server-side.md)**
651
310
 
652
- - Client sends
653
- `{"type": "subscribe" | "unsubscribe" | "publish", "topic", "payload"}`
654
- - Server broadcasts `{"topic", "payload"}` to subscribers of each topic
655
-
656
- This is just to show you how it works; if you want a simple self-hosted
657
- solution, use the `ws-relay` package explained above.
658
-
659
- ```js
660
- import {createTopicStrategy, toJson} from '@trystero-p2p/core'
661
-
662
- export const joinRoom = createTopicStrategy({
663
- // Define init as a function that returns a promise of your signaling client.
664
- // Resolve the promise when your client is ready to send messages.
665
- // You can also return an array of client promises for redundancy.
666
- // In this case, the client is a single WebSocket.
667
- init: config =>
668
- new Promise((resolve, reject) => {
669
- const ws = new WebSocket(config.relayConfig.urls[0])
670
- ws.addEventListener('open', () => resolve(ws), {once: true})
671
- ws.addEventListener('error', reject, {once: true})
672
- }),
673
-
674
- // Subscribe to one topic. Trystero decides which topics are needed and when.
675
- subscribeTopic: (client, topic, onMessage) => {
676
- const onWsMessage = event => {
677
- const message = JSON.parse(String(event.data))
678
-
679
- if (message.topic !== topic) {
680
- return
681
- }
682
-
683
- onMessage(message.topic, message.payload)
684
- }
685
-
686
- client.addEventListener('message', onWsMessage)
687
- client.send(toJson({type: 'subscribe', topic}))
688
-
689
- return () => {
690
- client.send(toJson({type: 'unsubscribe', topic}))
691
- client.removeEventListener('message', onWsMessage)
692
- }
693
- },
694
-
695
- // Publish a payload to one topic.
696
- publishTopic: (client, topic, payload) =>
697
- client.send(
698
- toJson({
699
- type: 'publish',
700
- topic,
701
- payload
702
- })
703
- )
704
- })
705
-
706
- const room = joinRoom(
707
- {appId: 'my-app-id', relayConfig: {urls: ['wss://my-relay.example']}},
708
- 'my-room-id'
709
- )
710
- ```
711
-
712
- `createTopicStrategy` uses a short startup announcement burst followed by a
713
- 60-second steady interval and repeats the burst after a peer drops. Set
714
- `reannounceOnDisconnect: false` when the signaling backend stores peer presence
715
- rather than delivering announcements as transient events.
716
-
717
- For non-pub/sub signaling protocols, such as trackers that exchange offers in
718
- bulk, `createStrategy` is available as a lower-level advanced API.
719
-
720
- ### Supabase setup
721
-
722
- To use the Supabase strategy:
311
+ ---
723
312
 
724
- 1. Create a [Supabase](https://supabase.com) project or use an existing one
725
- 2. On the dashboard, go to Project Settings -> API
726
- 3. Copy the Project URL and set that as the `appId` in the Trystero config, copy
727
- the `anon public` API key and set it as `relayConfig.supabaseKey` in the
728
- Trystero config
313
+ ## How does Trystero connect users with no backend?
729
314
 
730
- ### Firebase setup
315
+ WebRTC lets browsers talk directly to each other, peer-to-peer, without routing
316
+ traffic through a central server. The one catch with WebRTC has always been
317
+ _signaling_: two browsers need a brief introduction to swap connection info
318
+ before they can link up.
731
319
 
732
- If you want to use the Firebase strategy and don't have an existing project:
320
+ Trystero handles that introduction automatically out of the box using
321
+ decentralized public relays β€” nothing to deploy or configure. Once peers
322
+ connect, **your app's data never touches a relay**: state, messages, files, and
323
+ media flow straight from browser to browser, end-to-end encrypted.
733
324
 
734
- 1. Create a [Firebase](https://firebase.google.com/) project
735
- 2. Create a new Realtime Database
736
- 3. Copy the `databaseURL` and use it as the `appId` in your Trystero config
325
+ Prefer to run your own signaling setup with Nostr, MQTT, BitTorrent, Supabase,
326
+ Firebase, or a tiny self-hosted WebSocket relay? You can swap strategies with a
327
+ one-line import change β€” see the
328
+ **[Signaling & Relays guide](https://github.com/dmotz/trystero/blob/main/docs/signaling.md)**.
737
329
 
738
330
  <details>
739
- <summary>
740
- Optional: configure the database with security rules to limit activity:
741
- </summary>
742
-
743
- ```json
744
- {
745
- "rules": {
746
- ".read": false,
747
- ".write": false,
748
- "__trystero__": {
749
- ".read": false,
750
- ".write": false,
751
- "$room_id": {
752
- ".read": true,
753
- ".write": true
754
- }
755
- }
756
- }
757
- }
758
- ```
759
-
760
- These rules ensure room peer presence is only readable if the room namespace is
761
- known ahead of time.
331
+ <summary>Peek under the hood: how serverless discovery and security work</summary>
332
+
333
+ - **Encrypted signaling**: Connection offers (SDPs) are encrypted with
334
+ AES-256-GCM before touching any relay. Adding a room `password` derives the
335
+ key via WebCrypto `PBKDF2-SHA256` (600,000 iterations) so outsiders and relay
336
+ operators can't read handshakes or join the room.
337
+ - **Self-certifying identities**: Every user (`userId`) and browser tab
338
+ (`peerId`) is derived from a WebCrypto ECDSA P-256 public keypair. During
339
+ connection setup, peers verify each other's keys against the WebRTC DTLS
340
+ certificate fingerprint
341
+ ([RFC 8827](https://www.rfc-editor.org/rfc/rfc8827.html)), preventing spoofing
342
+ and man-in-the-middle relays.
343
+ - **Signed state & roles**: Updates to `makeUserState`, `makeState`, and
344
+ `roles()` carry cryptographic signatures and Hybrid Logical Clock timestamps,
345
+ allowing peers (including late joiners) to verify who wrote what without a
346
+ central server.
762
347
 
763
348
  </details>
764
349
 
765
- ## API
766
-
767
- ### `joinRoom(config, roomId, [callbacks])`
768
-
769
- Adds local user to room whereby other peers in the same namespace will open
770
- communication channels and send events. Calling `joinRoom()` multiple times with
771
- the same namespace will return the same room instance.
772
-
773
- - `config` - Configuration object containing the following keys:
774
- - `appId` - **(required)** A unique string identifying your app. When using
775
- Supabase, this should be set to your project URL (see
776
- [Supabase setup instructions](#supabase-setup)). If using Firebase, this
777
- should be the `databaseURL` from your Firebase config (also see
778
- `relayConfig.firebaseApp` below for an alternative way of configuring the
779
- Firebase strategy).
780
-
781
- - `password` - **(optional)** A string to encrypt session descriptions via
782
- AES-GCM as they are passed through the peering medium. If not set, session
783
- descriptions will be encrypted with a key derived from the app ID and room
784
- name. A custom password must match between any peers in the room for them to
785
- connect. See [encryption](#encryption) for more details.
786
-
787
- - `maxReceiveBytes` - **(optional)** Maximum payload size and budget for
788
- concurrent large incoming transfers per room, in bytes. Defaults to
789
- `256 * 1024 ** 2` (256 MiB). Must be a positive integer. Raise it for larger
790
- files or lower it to reduce memory use.
791
-
792
- - `passive` - **(optional)** Boolean for backup or relay peers that should
793
- listen for active peers without announcing themselves while a room is
794
- dormant. Passive peers activate only after hearing a non-passive peer and
795
- include a passive flag in their signaling so passive peers do not connect to
796
- each other. For BitTorrent, dormant passive rooms announce as seeders
797
- (`left: 0`) without offers, which avoids passive-to-passive discovery while
798
- keeping tracker load low.
799
-
800
- - `relayConfig` - **(optional unless required by your chosen strategy)**
801
- Object containing strategy-specific relay settings:
802
- - `urls` - **(optional for 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT; required for πŸ”Œ
803
- WebSocket relay)** Custom list of URLs for the strategy to use to
804
- bootstrap P2P connections. These would be BitTorrent trackers, Nostr
805
- relays, MQTT brokers, and WebSocket relays, respectively. They must
806
- support secure WebSocket connections.
807
-
808
- - `redundancy` - **(optional, 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT only)**
809
- Integer specifying how many default relay endpoints to connect to
810
- simultaneously. Passing a `urls` option will cause this option to be
811
- ignored as the entire list will be used.
812
-
813
- - `manualReconnection` - **(optional, 🐦 Nostr and 🌊 BitTorrent only)**
814
- Boolean (default: `false`) that when set to `true` disables automatically
815
- pausing and resuming reconnection attempts when the browser goes offline
816
- and comes back online. This is useful if you want to manage this behavior
817
- yourself.
818
-
819
- - `warnOnRelayFailure` - **(optional)** Boolean (default: `true`) that when
820
- set to `false` suppresses warnings caused by relay failures. This includes
821
- failed announce attempts and strategy-specific failure messages from
822
- relays or trackers.
823
-
824
- - `supabaseKey` - **(required, ⚑️ Supabase only)** Your Supabase project's
825
- `anon public` API key.
826
-
827
- - `firebaseApp` - **(optional, πŸ”₯ Firebase only)** You can pass an already
828
- initialized Firebase app instance instead of an `appId`. Normally Trystero
829
- will initialize a Firebase app based on the `appId` but this will fail if
830
- you've already initialized it for use elsewhere.
831
-
832
- - `firebasePath` - **(optional, πŸ”₯ Firebase only)** String specifying path
833
- where Trystero writes its matchmaking data in your database
834
- (`'__trystero__'` by default). Changing this is useful if you want to run
835
- multiple apps using the same database and don't want to worry about
836
- namespace collisions.
837
-
838
- - `rtcConfig` - **(optional)** Specifies a custom
839
- [`RTCConfiguration`](https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration)
840
- for all peer connections.
841
-
842
- - `trickleIce` - **(optional)** Boolean controlling whether ICE candidates are
843
- sent incrementally (`true`) or bundled with SDP (`false`). Default is
844
- strategy-dependent: `true` for most strategies, `false` for BitTorrent and
845
- IPFS unless explicitly set.
846
-
847
- - `turnConfig` - **(optional)** Specifies a custom list of TURN servers to use
848
- (see
849
- [Troubleshooting connection issues](#troubleshooting-connection-issues)).
850
- Each item in the list should correspond to an
851
- [ICE server config object](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers).
852
- When passing a TURN config like this, Trystero's default STUN servers will
853
- also be used. To override this and use both custom STUN and TURN servers,
854
- instead pass the config via the above `rtcConfig.iceServers` option as a
855
- list of both STUN/TURN servers β€” this won't inherit Trystero's defaults.
856
-
857
- - `rtcPolyfill` - **(optional)** Use this to pass a custom
858
- [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection)-compatible
859
- constructor. This is useful for running outside of a browser, such as in
860
- Node (still experimental).
861
-
862
- - `roomId` - A string to namespace peers and events within a room.
863
-
864
- - `callbacks` - **(optional)** Callback config object containing:
865
- - `onJoinError(details)` - Called when room join fails due to an incorrect
866
- password, when handshake admission fails (including timeout), or when peers
867
- exchange SDP but WebRTC still cannot establish a direct connection. This
868
- last case usually means TURN servers are needed or misconfigured (see
869
- [Troubleshooting connection issues](#troubleshooting-connection-issues)).
870
- `details` is an object containing `appId`, `roomId`, `peerId`, and `error`
871
- describing the failure.
872
-
873
- - `onPeerHandshake(peerId, send, receive, isInitiator)` - Async predicate that
874
- runs after the transport connects but before the peer becomes active.
875
- Return/resolve to accept the peer, throw/reject to deny the peer.
876
- - `peerId` - ID of the pending peer.
877
- - `send(data, [metadata])` - Sends handshake payloads to the pending peer.
878
- - `receive()` - Resolves to the next handshake message from the peer as
879
- `{data, metadata}`.
880
- - `isInitiator` - Deterministic role flag for avoiding protocol deadlocks.
881
-
882
- - `handshakeTimeoutMs` - Timeout for pending handshakes in milliseconds
883
- (`10000` by default). If exceeded, the peer is denied and `onJoinError` is
884
- called.
885
-
886
- During handshake, the peer remains pending and is not included in `getPeers()`
887
- and does not trigger Trystero API events (`onPeerJoin`, action receivers,
888
- stream/track callbacks). Non-handshake data received while pending is dropped.
889
-
890
- Minimal handshake example:
891
-
892
- ```js
893
- import {joinRoom} from 'trystero'
894
-
895
- const room = joinRoom({appId: 'my-app'}, 'secure-room', {
896
- onPeerHandshake: async (_, send, receive, isInitiator) => {
897
- if (isInitiator) {
898
- await send({challenge: 'prove-you-know-the-secret'})
899
- const {data} = await receive()
900
-
901
- if (data?.response !== 'shared-secret') {
902
- throw new Error('handshake rejected')
903
- }
904
- } else {
905
- const {data} = await receive()
906
-
907
- if (data?.challenge !== 'prove-you-know-the-secret') {
908
- throw new Error('handshake rejected')
909
- }
910
-
911
- await send({response: 'shared-secret'})
912
- }
913
- }
914
- })
915
- ```
916
-
917
- Returns an object with the following methods:
918
-
919
- - ### `leave()`
920
-
921
- Remove local user from room and unsubscribe from room events.
922
-
923
- - ### `getPeers()`
924
-
925
- Returns a map of
926
- [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection)s
927
- for the peers present in room (not including the local user). The keys of this
928
- object are the respective peers' IDs.
929
-
930
- - ### `isPassive()`
931
-
932
- Returns whether the room was joined with `passive: true`.
933
-
934
- - ### `addStream(stream, [options])`
935
-
936
- Broadcasts media stream to other peers.
937
- - `stream` - A `MediaStream` with audio and/or video to send to peers in the
938
- room.
939
-
940
- - `options.target` - **(optional)** If specified, the stream is sent only to
941
- the target peer ID (string) or list of peer IDs (array). Passing `null` or
942
- omitting this option sends to all peers in the room.
943
-
944
- - `options.metadata` - **(optional)** Additional metadata (any serializable
945
- type) to be sent with the stream. This is useful when sending multiple
946
- streams so recipients know which is which (e.g. a webcam versus a screen
947
- capture).
948
-
949
- - ### `removeStream(stream, [options])`
950
-
951
- Stops sending previously sent media stream to other peers.
952
- - `stream` - A previously sent `MediaStream` to stop sending.
953
-
954
- - `options.target` - **(optional)** If specified, the stream is removed only
955
- from the target peer ID (string) or list of peer IDs (array).
956
-
957
- - ### `addTrack(track, stream, [options])`
958
-
959
- Adds a new media track to a stream.
960
- - `track` - A `MediaStreamTrack` to add to an existing stream.
961
-
962
- - `stream` - The target `MediaStream` to attach the new track to.
963
-
964
- - `options.target` - **(optional)** If specified, the track is sent only to
965
- the target peer ID (string) or list of peer IDs (array).
966
-
967
- - `options.metadata` - **(optional)** Additional metadata (any serializable
968
- type) to be sent with the track. See `metadata` notes for `addStream()`
969
- above for more details.
970
-
971
- - ### `removeTrack(track, [options])`
972
-
973
- Removes a media track.
974
- - `track` - The `MediaStreamTrack` to remove.
975
-
976
- - `options.target` - **(optional)** If specified, the track is removed only
977
- from the target peer ID (string) or list of peer IDs (array).
978
-
979
- - ### `replaceTrack(oldTrack, newTrack, [options])`
980
-
981
- Replaces a media track with a new one.
982
- - `oldTrack` - The `MediaStreamTrack` to remove.
983
-
984
- - `newTrack` - A `MediaStreamTrack` to attach.
985
-
986
- - `options.target` - **(optional)** If specified, the track is replaced only
987
- for the target peer ID (string) or list of peer IDs (array).
988
-
989
- - `options.metadata` - **(optional)** Additional metadata (any serializable
990
- type) to be sent with the replacement track.
991
-
992
- - ### `onPeerJoin`
993
-
994
- A callback property that will be called when a peer joins the room. Assigning
995
- a new function replaces the previous handler; assigning `null` clears it.
996
- Existing active peers are immediately replayed to a newly assigned handler.
997
- - `callback(peerId)` - Function to run whenever a peer joins, called with the
998
- peer's ID.
999
-
1000
- Example:
1001
-
1002
- ```js
1003
- room.onPeerJoin = peerId => console.log(`${peerId} joined`)
1004
- ```
1005
-
1006
- - ### `onPeerLeave`
1007
-
1008
- A callback property that will be called when a peer leaves the room. Assigning
1009
- a new function replaces the previous handler; assigning `null` clears it.
1010
- - `callback(peerId)` - Function to run whenever a peer leaves, called with the
1011
- peer's ID.
1012
-
1013
- Example:
1014
-
1015
- ```js
1016
- room.onPeerLeave = peerId => console.log(`${peerId} left`)
1017
- ```
1018
-
1019
- - ### `onPeerStream`
1020
-
1021
- A callback property that will be called when a peer sends a media stream.
1022
- Assigning a new function replaces the previous handler; assigning `null`
1023
- clears it.
1024
- - `callback(stream, peerId, metadata)` - Function to run whenever a peer sends
1025
- a media stream, called with the the peer's stream, ID, and optional metadata
1026
- (see `addStream()` above for details).
1027
-
1028
- Example:
1029
-
1030
- ```js
1031
- room.onPeerStream = (stream, peerId) =>
1032
- console.log(`got stream from ${peerId}`, stream)
1033
- ```
1034
-
1035
- - ### `onPeerTrack`
1036
-
1037
- A callback property that will be called when a peer sends a media track.
1038
- Assigning a new function replaces the previous handler; assigning `null`
1039
- clears it.
1040
- - `callback(track, stream, peerId, metadata)` - Function to run whenever a
1041
- peer sends a media track, called with the the peer's track, attached stream,
1042
- ID, and optional metadata (see `addTrack()` above for details).
1043
-
1044
- Example:
1045
-
1046
- ```js
1047
- room.onPeerTrack = (track, stream, peerId) =>
1048
- console.log(`got track from ${peerId}`, track)
1049
- ```
1050
-
1051
- - ### `makeAction(actionId, [config])`
1052
-
1053
- Listen for and send custom data actions.
1054
- - `actionId` - A string to register this action consistently among all peers.
1055
-
1056
- If `config.kind` is omitted, the action is a one-way message action. Passing
1057
- `kind: 'request'` creates a request/response action.
1058
-
1059
- Message actions expose:
1060
- - `send(data, [options])` - Sends data to peers and resolves when local
1061
- sending is complete. To cancel an in-progress send, pass an
1062
- `AbortController`'s `signal` in the options (e.g.
1063
- `send(data, {signal: controller.signal})`) and call `controller.abort()`.
1064
- - `onMessage` - Nullable callback property for received messages.
1065
- - `onReceiveProgress` - Nullable callback property for inbound progress.
1066
-
1067
- Request actions expose:
1068
- - `request(data, options)` - Sends to one peer and resolves with its response.
1069
- - `requestMany(data, options)` - Sends to many peers and resolves with
1070
- peer-labeled settled results.
1071
- - `onRequest` - Nullable callback property that returns the response.
1072
- - `onReceiveProgress` - Nullable callback property for inbound progress.
1073
-
1074
- Both kinds expose `onReceive`, a nullable callback accepting
1075
- `{byteLength, peerId, metadata, kind, signal}` and returning a boolean or a
1076
- promise of a boolean. It can also be provided in `config`. `kind` is
1077
- `'message'`, `'request'`, or `'response'`; request actions apply it to both
1078
- requests and responses. Responses have no metadata. See
1079
- [receiving files](#receiving-files).
1080
-
1081
- Send options use `target`, `metadata`, `onProgress`, and `signal`. Request
1082
- options use `target`, `metadata`, `timeoutMs`, `onProgress`, and `signal`.
1083
- `requestMany()` uses `targets` plus optional `onResult` for each peer result
1084
- as it arrives. Request timeouts cover approval, sending, and the response.
1085
-
1086
- Example:
1087
-
1088
- ```js
1089
- const cursor = room.makeAction('cursormove')
1090
-
1091
- window.addEventListener('mousemove', e => cursor.send([e.clientX, e.clientY]))
1092
-
1093
- cursor.onMessage = ([x, y], {peerId}) => {
1094
- const peerCursor = cursorMap[peerId]
1095
- peerCursor.style.left = x + 'px'
1096
- peerCursor.style.top = y + 'px'
1097
- }
1098
- ```
1099
-
1100
- - ### `ping(peerId)`
1101
-
1102
- Takes a peer ID and returns a promise that resolves to the milliseconds the
1103
- round-trip to that peer took. Use this for measuring latency.
1104
- - `peerId` - Peer ID string of the target peer.
1105
-
1106
- Example:
1107
-
1108
- ```js
1109
- // log round-trip time every 2 seconds
1110
- room.onPeerJoin = peerId =>
1111
- setInterval(
1112
- async () => console.log(`took ${await room.ping(peerId)}ms`),
1113
- 2000
1114
- )
1115
- ```
1116
-
1117
- ### `selfId`
1118
-
1119
- A unique ID string other peers will know the local user as globally across
1120
- rooms.
1121
-
1122
- ### `getRelaySockets()`
1123
-
1124
- **(🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT, πŸ”Œ WebSocket relay only)** Returns an
1125
- object of relay URL keys mapped to their WebSocket connections. This can be
1126
- useful for determining the state of the user's connection to the relays and
1127
- handling any connection failures.
1128
-
1129
- Example:
1130
-
1131
- ```js
1132
- import {getRelaySockets} from '@trystero-p2p/torrent'
1133
-
1134
- console.log(getRelaySockets())
1135
- // => Object {
1136
- // "wss://tracker.webtorrent.dev": WebSocket,
1137
- // "wss://tracker.openwebtorrent.com": WebSocket
1138
- // }
1139
- ```
1140
-
1141
- ### `pauseRelayReconnection()`
1142
-
1143
- **(🐦 Nostr, 🌊 BitTorrent only)** Normally Trystero will try to automatically
1144
- reconnect to relay sockets unless `relayConfig.manualReconnection: true` is set
1145
- in the room config. Calling this function stops relay reconnection attempts
1146
- until `resumeRelayReconnection()` is called.
1147
-
1148
- ### `resumeRelayReconnection()`
1149
-
1150
- **(🐦 Nostr, 🌊 BitTorrent only)** Allows relay reconnection attempts to resume.
1151
- (See `pauseRelayReconnection()` above).
1152
-
1153
- ## Which strategy should I choose?
1154
-
1155
- By default Trystero uses the Nostr network which is highly decentralized with
1156
- hundreds of active relays running. This is a good choice if you're interested in
1157
- decentralization and high redundancy. The other decentralized strategies are
1158
- recommended in the order of MQTT, BitTorrent, and IPFS, based on robustness.
1159
- These networks have far less relay redundancy than Nostr, but you might prefer
1160
- them for other reasons. You can of course host your own relay server for any of
1161
- these strategies.
350
+ ---
1162
351
 
1163
- For a middleground between using public relays and self-hosting, the built-in
1164
- Supabase and Firebase strategies are a good option.
352
+ ## Documentation
353
+
354
+ Dive deeper into any feature or browse the full API:
355
+
356
+ | Guide | What's inside |
357
+ | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
358
+ | **[Shared & Per-User State](https://github.com/dmotz/trystero/blob/main/docs/state.md)** | `makeUserState`, `makeState`, validation rules, late-joiner sync, and React (`useSyncExternalStore`) |
359
+ | **[Identity, Presence & Room Access](https://github.com/dmotz/trystero/blob/main/docs/identity.md)** | `userId` vs `peerId`, multi-tab presence, custom identity keys, room `password`s, `maxUsers`, and `onError` |
360
+ | **[Admins, Roles & Moderation](https://github.com/dmotz/trystero/blob/main/docs/roles.md)** | `roles()`, self-certifying rooms, fixed admins, custom roles, authoritative state, and banning |
361
+ | **[Actions, RPC & File Transfers](https://github.com/dmotz/trystero/blob/main/docs/actions.md)** | `makeAction`, `send`, `request` & `requestMany`, TypeScript types, binary metadata, progress bars, and `onReceive` |
362
+ | **[Audio, Video & Screen Sharing](https://github.com/dmotz/trystero/blob/main/docs/media.md)** | `addStream`, `onPeerStream`, `removeStream`, camera/screen switching (`replaceTrack`), and stream metadata |
363
+ | **[Scaling Large Rooms](https://github.com/dmotz/trystero/blob/main/docs/scaling.md)** | Multi-hop routing with `scale()`, direct media/file promotion, and `getTopology()` |
364
+ | **[Signaling & Relays](https://github.com/dmotz/trystero/blob/main/docs/signaling.md)** | Default zero-config relays, Supabase, Firebase, MQTT, BitTorrent, self-hosted `ws-relay`, custom strategies, and bundle sizes |
365
+ | **[Running in Node, Bun & Deno](https://github.com/dmotz/trystero/blob/main/docs/server-side.md)** | Headless peers, Raspberry Pi / IoT setups, and `passive` state backup peers |
366
+ | **[NAT Traversal & Troubleshooting](https://github.com/dmotz/trystero/blob/main/docs/troubleshooting.md)** | Configuring TURN servers, diagnosing strict firewalls, and `ping()` |
367
+ | **[Full API Reference](https://github.com/dmotz/trystero/blob/main/docs/api.md)** | Complete function signatures, TypeScript types, config options, and error codes |
368
+ | **[Upgrading to 0.27.0](https://github.com/dmotz/trystero/blob/main/MIGRATION.md)** | Architectural overview, breaking changes, and before/after migration examples |
1165
369
 
1166
370
  ---
1167
371