Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

✨🤝✨ Build instant multiplayer webapps, no server required — Magic WebRTC matchmaking over BitTorrent, Nostr, MQTT, IPFS, Supabase, and Firebase

License

NotificationsYou must be signed in to change notification settings

dmotz/trystero

Repository files navigation

Build instant multiplayer web apps, no server required

👉TRY THE DEMO 👈

Trystero manages a clandestine courier network that lets your application'susers talk directly with one another, encrypted and without a server middleman.

The net is full of open, decentralized communication channels: torrent trackers,IoT device brokers, boutique file protocols, and niche social networks.

Trystero piggybacks on these networks to automatically establish secure,private, P2P connections between your app's users with no effort on your part.

Peers can connect via🌊 BitTorrent, 🐦 Nostr, 📡 MQTT, ⚡️ Supabase, 🔥 Firebase, or 🪐 IPFS– all using the same API.

Besides making peer matching automatic, Trystero offers some nice abstractionson top of WebRTC:

  • 👂📣 Rooms / broadcasting
  • 🔢📩 Automatic serialization / deserialization of data
  • 🎥🏷 Attach metadata to binary data and media streams
  • ✂️⏳ Automatic chunking and throttling of large data
  • ⏱🤞 Progress events and promises for data transfers
  • 🔐📝 Session data encryption
  • ⚛️🪝 React hooks

You can see what people are building with Trysterohere.


Contents


How it works

👉If you just want to try out Trystero, you can skip this explainer andjump into using it.

To establish a direct peer-to-peer connection with WebRTC, a signalling channelis needed to exchange peer information(SDP). Typicallythis involves running your own matchmaking server but Trystero abstracts thisaway for you and offers multiple "serverless" strategies for connecting peers(currently BitTorrent, Nostr, MQTT, Supabase, Firebase, and IPFS).

The important point to remember is this:

🔒

Beyond peer discovery, your app's data never touches the strategy medium andis sent directly peer-to-peer and end-to-end encrypted between users.

👆

You cancompare strategies here.

Get started

You can install with npm (npm i trystero) and import like so:

import{joinRoom}from'trystero'

Or maybe you prefer a simple script tag? Download a pre-built JS file from thelatest release and importit locally:

<scripttype="module">import{joinRoom}from'./trystero-torrent.min.js'</script>

By default, theNostr strategy is used. To use adifferent one just deep import like so (your bundler should handle includingonly relevant code):

import{joinRoom}from'trystero/mqtt'// (trystero-mqtt.min.js with a local file)// orimport{joinRoom}from'trystero/torrent'// (trystero-torrent.min.js)// orimport{joinRoom}from'trystero/supabase'// (trystero-supabase.min.js)// orimport{joinRoom}from'trystero/firebase'// (trystero-firebase.min.js)// orimport{joinRoom}from'trystero/ipfs'// (trystero-ipfs.min.js)

Next, join the user to a room with an ID:

constconfig={appId:'san_narciso_3d'}constroom=joinRoom(config,'yoyodyne')

The first argument is a configuration object that requires anappId. Thisshould be a completely unique identifier for your app¹. The second argumentis the room ID.

Why rooms? Browsers can only handle a limited amount of WebRTC connections ata time so it's recommended to design your app such that users are divided intogroups (or rooms, or namespaces, or channels... whatever you'd like to callthem).

¹ When using Firebase,appId should be yourdatabaseURL and when usingSupabase, it should be your project URL.

Listen for events

Listen for peers joining the room:

room.onPeerJoin(peerId=>console.log(`${peerId} joined`))

Listen for peers leaving the room:

room.onPeerLeave(peerId=>console.log(`${peerId} left`))

Listen for peers sending their audio/video streams:

room.onPeerStream((stream,peerId)=>(peerElements[peerId].video.srcObject=stream))

To unsubscribe from events, leave the room:

room.leave()

You can access the local user's peer ID by importingselfId like so:

import{selfId}from'trystero'console.log(`my peer ID is${selfId}`)

Broadcast events

Send peers your video stream:

room.addStream(awaitnavigator.mediaDevices.getUserMedia({audio:true,video:true}))

Send and subscribe to custom P2P actions:

const[sendDrink,getDrink]=room.makeAction('drink')// buy drink for a friendsendDrink({drink:'negroni',withIce:true},friendId)// buy round for the house (second argument omitted)sendDrink({drink:'mezcal',withIce:false})// listen for drinks sent to yougetDrink((data,peerId)=>console.log(`got a${data.drink} with${data.withIce ?'' :'out'} ice from${peerId}`))

You can also use actions to send binary data, like images:

const[sendPic,getPic]=room.makeAction('pic')// blobs are automatically handled, as are any form of TypedArraycanvas.toBlob(blob=>sendPic(blob))// binary data is received as raw ArrayBuffers so your handling code should// interpret it in a way that makes sensegetPic((data,peerId)=>(imgs[peerId].src=URL.createObjectURL(newBlob([data]))))

Let's say we want users to be able to name themselves:

constidsToNames={}const[sendName,getName]=room.makeAction('name')// tell other peers currently in the room our namesendName('Oedipa')// tell newcomersroom.onPeerJoin(peerId=>sendName('Oedipa',peerId))// listen for peers naming themselvesgetName((name,peerId)=>(idsToNames[peerId]=name))room.onPeerLeave(peerId=>console.log(`${idsToNames[peerId]||'a weird stranger'} left`))

Actions are smart and handle serialization and chunking for you behind thescenes. This means you can send very large files and whatever data you sendwill be received on the other side as the same type (a number as a number,a string as a string, an object as an object, binary as binary, etc.).

Audio and video

Here's a simple example of how you could create an audio chatroom:

// this object can store audio instances for laterconstpeerAudios={}// get a local audio stream from the microphoneconstselfStream=awaitnavigator.mediaDevices.getUserMedia({audio:true,video:false})// send stream to peers currently in the roomroom.addStream(selfStream)// send stream to peers who join laterroom.onPeerJoin(peerId=>room.addStream(selfStream,peerId))// handle streams from other peersroom.onPeerStream((stream,peerId)=>{// create an audio instance and set the incoming streamconstaudio=newAudio()audio.srcObject=streamaudio.autoplay=true// add the audio to peerAudio object if you want to address it for something// later (volume, etc.)peerAudios[peerId]=audio})

Doing the same with video is similar, just be sure to add incoming streams tovideo elements in the DOM:

constpeerVideos={}constvideoContainer=document.getElementById('videos')room.onPeerStream((stream,peerId)=>{letvideo=peerVideos[peerId]// if this peer hasn't sent a stream before, create a video elementif(!video){video=document.createElement('video')video.autoplay=true// add video element to the DOMvideoContainer.appendChild(video)}video.srcObject=streampeerVideos[peerId]=video})

Advanced

Binary metadata

Let's say your app supports sending various types of files and you want toannotate the raw bytes being sent with metadata about how they should beinterpreted. Instead of manually adding metadata bytes to the buffer you cansimply pass a metadata argument in the sender action for your binary payload:

const[sendFile,getFile]=makeAction('file')getFile((data,peerId,metadata)=>console.log(`got a file (${metadata.name}) from${peerId} with type${metadata.type}`,data))// to send metadata, pass a third argument// to broadcast to the whole room, set the second peer ID argument to nullsendFile(buffer,null,{name:'The Courierʼs Tragedy',type:'application/pdf'})

Action promises

Action sender functions return a promise that resolves when they're donesending. You can optionally use this to indicate to the user when a largetransfer is done.

awaitsendFile(amplePayload)console.log('done sending to all peers')

Progress updates

Action sender functions also take an optional callback function that will becontinuously called as the transmission progresses. This can be used for showinga progress bar to the sender for large tranfers. The callback is called with apercentage value between 0 and 1 and the receiving peer's ID:

sendFile(payload,// notice the peer target argument for any action sender can be a single peer// ID, an array of IDs, or null (meaning send to all peers in the room)[peerIdA,peerIdB,peerIdC],// metadata, which can also be null if you're only interested in the// progress handler{filename:'paranoids.flac'},// assuming each peer has a loading bar added to the DOM, its value is// updated here(percent,peerId)=>(loadingBars[peerId].value=percent))

Similarly you can listen for progress events as a receiver like this:

const[sendFile,getFile,onFileProgress]=room.makeAction('file')onFileProgress((percent,peerId,metadata)=>console.log(`${percent*100}% done receiving${metadata.filename} from${peerId}`))

Notice that any metadata is sent with progress events so you can show thereceiving user that there is a transfer in progress with perhaps the name of theincoming file.

Since a peer can send multiple transmissions in parallel, you can also usemetadata to differentiate between them, e.g. by sending a unique ID.

Encryption

Once peers are connected to each other all of their communications areend-to-end encrypted. During the initial connection / discovery process, peers'SDPs are sent viathe chosen peering strategy medium. By default the SDP is encrypted using a keyderived from your app ID and room ID to prevent plaintext session data fromappearing in logs. This is fine for most use cases, however a relay strategyoperator can reverse engineer the key using the room and app IDs. A more secureoption is to pass apassword parameter in the app configuration object whichwill be used to derive the encryption key:

joinRoom({appId:'kinneret',password:'MuchoMaa$'},'w_a_s_t_e__v_i_p')

This is a shared secret that must be known ahead of time and the password mustmatch for all peers in the room for them to be able to connect. An example usecase might be a private chat room where users learn the password via externalmeans.

React hooks

Trystero functions are idempotent so they already work out of the box as Reacthooks.

Here's a simple example component where each peer syncs their favoritecolor to everyone else:

import{joinRoom}from'trystero'import{useState}from'react'consttrysteroConfig={appId:'thurn-und-taxis'}exportdefaultfunctionApp({roomId}){constroom=joinRoom(trysteroConfig,roomId)const[sendColor,getColor]=room.makeAction('color')const[myColor,setMyColor]=useState('#c0ffee')const[peerColors,setPeerColors]=useState({})// whenever new peers join the room, send my color to them:room.onPeerJoin(peer=>sendColor(myColor,peer))// listen for peers sending their colors and update the state accordingly:getColor((color,peer)=>setPeerColors(peerColors=>({...peerColors,[peer]:color})))constupdateColor=e=>{const{value}=e.target// when updating my own color, broadcast it to all peers:sendColor(value)setMyColor(value)}return(<><h1>Trystero + React</h1><h2>My color:</h2><inputtype="color"value={myColor}onChange={updateColor}/><h2>Peer colors:</h2><ul>{Object.entries(peerColors).map(([peerId,color])=>(<likey={peerId}style={{backgroundColor:color}}>{peerId}:{color}</li>))}</ul></>)}

Astute readers may notice the above example is simple and doesn't consider if wewant to change the component's room ID or unmount it. For those scenarios youcan use this simpleuseRoom() hook that unsubscribes from room eventsaccordingly:

import{joinRoom}from'trystero'import{useEffect,useRef}from'react'exportconstuseRoom=(roomConfig,roomId)=>{constroomRef=useRef(joinRoom(roomConfig,roomId))constlastRoomIdRef=useRef(roomId)useEffect(()=>{if(roomId!==lastRoomIdRef.current){roomRef.current.leave()roomRef.current=joinRoom(roomConfig,roomId)lastRoomIdRef.current=roomId}return()=>roomRef.current.leave()},[roomConfig,roomId])returnroomRef.current}

Connection issues

WebRTC is powerful but some networks simply don't allow direct P2P connectionsusing it. If you find that certain user pairings aren't working in Trystero,you're likely encountering an issue at the network provider level. To solve thisyou can configure a TURN server which will act as a proxy layer for peersthat aren't able to connect directly to one another.

  1. If you can, confirm that the issue is specific to particular networkconditions (e.g. user with ISP A cannot connect to a user with ISP B). Ifother user pairings are working (like those between two browsers on the samemachine), this likely confirms that Trystero is working correctly.
  2. Sign up for a TURN service or host your own. There are various hosted TURNservices you can find online (likeOpen Relay orCloudflare), some with freetiers. You can also host an open source TURN server likecoturn,Pion TURN,Violet, oreturnal.
  3. Once you have a TURN server, configure Trystero with it like this:
    constroom=joinRoom({// ...your app configturnConfig:[{// single string or list of strings of urls to access TURN serverurls:['turn:your-turn-server.ok:1979'],username:'username',credential:'password'}]},'roomId')

Supabase setup

To use the Supabase strategy:

  1. Create aSupabase project or use an existing one
  2. On the dashboard, go to Project Settings -> API
  3. Copy the Project URL and set that as theappId in the Trystero config,copy theanon public API key and set it assupabaseKey in the Trysteroconfig

Firebase setup

If you want to use the Firebase strategy and don't have an existing project:

  1. Create aFirebase project
  2. Create a new Realtime Database
  3. Copy thedatabaseURL and use it as theappId in your Trystero config
[*Optional*] Configure the database with security rules to limit activity:
{"rules": {".read":false,".write":false,"__trystero__": {".read":false,".write":false,"$room_id": {".read":true,".write":true      }    }  }}

These rules ensure room peer presence is only readable if the room namespace isknown ahead of time.

API

joinRoom(config, roomId, [onError])

Adds local user to room whereby other peers in the same namespace will opencommunication channels and send events. CallingjoinRoom() multiple times withthe same namespace will return the same room instance.

  • config - Configuration object containing the following keys:

    • appId -(required) A unique string identifying your app. When usingSupabase, this should be set to your project URL (seeSupabase setup instructions). If usingFirebase, this should be thedatabaseURL from your Firebase config (alsoseefirebaseApp below for an alternative way of configuring the Firebasestrategy).

    • password -(optional) A string to encrypt session descriptions viaAES-GCM as they are passed through the peering medium. If not set, sessiondescriptions will be encrypted with a key derived from the app ID and roomname. A custom password must match between any peers in the room for them toconnect. Seeencryption for more details.

    • relayUrls -(optional, 🌊 BitTorrent, 🐦 Nostr, 📡 MQTT only) Customlist of URLs for the strategy to use to bootstrap P2P connections. Thesewould be BitTorrent trackers, Nostr relays, and MQTT brokers, respectively.They must support secure WebSocket connections.

    • relayRedundancy -(optional, 🌊 BitTorrent, 🐦 Nostr, 📡 MQTT only)Integer specifying how many torrent trackers to connect to simultaneously incase some fail. Passing arelayUrls option will cause this option to beignored as the entire list will be used.

    • rtcConfig -(optional) Specifies a customRTCConfigurationfor all peer connections.

    • turnConfig -(optional) Specifies a custom list of TURN servers to use(seeConnection issues section). Each item in the listshould correspond to anICE server config object.When passing a TURN config like this, Trystero's default STUN servers willalso be used. To override this and use both custom STUN and TURN servers,instead pass the config via the abovertcConfig.iceServers option as alist of both STUN/TURN servers — this won't inherit Trystero's defaults.

    • rtcPolyfill -(optional) Use this to pass a customRTCPeerConnection-compatibleconstructor. This is useful for running outside of a browser, such as inNode (still experimental).

    • supabaseKey -(required, ⚡️ Supabase only) Your Supabase project'sanon public API key.

    • firebaseApp -(optional, 🔥 Firebase only) You can pass an alreadyinitialized Firebase app instance instead of anappId. Normally Trysterowill initialize a Firebase app based on theappId but this will fail ifyouʼve already initialized it for use elsewhere.

    • rootPath -(optional, 🔥 Firebase only) String specifying path whereTrystero writes its matchmaking data in your database ('__trystero__' bydefault). Changing this is useful if you want to run multiple apps using thesame database and don't want to worry about namespace collisions.

    • libp2pConfig -(optional, 🪐 IPFS only)Libp2pOptionswhere you can specify a list of static peers for bootstrapping.

  • roomId - A string to namespace peers and events within a room.

  • onError(details) -(optional) A callback function that will be called ifthe room cannot be joined due to an incorrect password.details is anobject containingappId,roomId,peerId, anderror describing theerror.

Returns an object with the following methods:

  • leave()

    Remove local user from room and unsubscribe from room events.

  • getPeers()

    Returns a map ofRTCPeerConnectionsfor the peers present in room (not including the local user). The keys ofthis object are the respective peers' IDs.

  • addStream(stream, [targetPeers], [metadata])

    Broadcasts media stream to other peers.

    • stream - AMediaStream with audio and/or video to send to peers in theroom.

    • targetPeers -(optional) If specified, the stream is sent only to thetarget peer ID (string) or list of peer IDs (array).

    • metadata -(optional) Additional metadata (any serializable type) tobe sent with the stream. This is useful when sending multiple streams sorecipients know which is which (e.g. a webcam versus a screen capture). Ifyou want to broadcast a stream to all peers in the room with a metadataargument, passnull as the second argument.

  • removeStream(stream, [targetPeers])

    Stops sending previously sent media stream to other peers.

    • stream - A previously sentMediaStream to stop sending.

    • targetPeers -(optional) If specified, the stream is removed only fromthe target peer ID (string) or list of peer IDs (array).

  • addTrack(track, stream, [targetPeers], [metadata])

    Adds a new media track to a stream.

    • track - AMediaStreamTrack to add to an existing stream.

    • stream - The targetMediaStream to attach the new track to.

    • targetPeers -(optional) If specified, the track is sent only to thetarget peer ID (string) or list of peer IDs (array).

    • metadata -(optional) Additional metadata (any serializable type) tobe sent with the track. Seemetadata notes foraddStream() above formore details.

  • removeTrack(track, stream, [targetPeers])

    Removes a media track from a stream.

    • track - TheMediaStreamTrack to remove.

    • stream - TheMediaStream the track is attached to.

    • targetPeers -(optional) If specified, the track is removed only fromthe target peer ID (string) or list of peer IDs (array).

  • replaceTrack(oldTrack, newTrack, stream, [targetPeers])

    Replaces a media track with a new one.

    • oldTrack - TheMediaStreamTrack to remove.

    • newTrack - AMediaStreamTrack to attach.

    • stream - TheMediaStream theoldTrack is attached to.

    • targetPeers -(optional) If specified, the track is replaced only forthe target peer ID (string) or list of peer IDs (array).

  • onPeerJoin(callback)

    Registers a callback function that will be called when a peer joins the room.If called more than once, only the latest callback registered is ever called.

    • callback(peerId) - Function to run whenever a peer joins, called with thepeer's ID.

    Example:

    onPeerJoin(peerId=>console.log(`${peerId} joined`))
  • onPeerLeave(callback)

    Registers a callback function that will be called when a peer leaves the room.If called more than once, only the latest callback registered is ever called.

    • callback(peerId) - Function to run whenever a peer leaves, called with thepeer's ID.

    Example:

    onPeerLeave(peerId=>console.log(`${peerId} left`))
  • onPeerStream(callback)

    Registers a callback function that will be called when a peer sends a mediastream. If called more than once, only the latest callback registered is evercalled.

    • callback(stream, peerId, metadata) - Function to run whenever a peer sendsa media stream, called with the the peer's stream, ID, and optional metadata(seeaddStream() above for details).

    Example:

    onPeerStream((stream,peerId)=>console.log(`got stream from${peerId}`,stream))
  • onPeerTrack(callback)

    Registers a callback function that will be called when a peer sends a mediatrack. If called more than once, only the latest callback registered is evercalled.

    • callback(track, stream, peerId, metadata) - Function to run whenever apeer sends a media track, called with the the peer's track, attached stream,ID, and optional metadata (seeaddTrack() above for details).

    Example:

    onPeerTrack((track,stream,peerId)=>console.log(`got track from${peerId}`,track))
  • makeAction(actionId)

    Listen for and send custom data actions.

    • actionId - A string to register this action consistently among all peers.

    Returns an array of three functions:

    1. Sender

      • Sends data to peers and returns a promise that resolves when alltarget peers are finished receiving data.

      • (data, [targetPeers], [metadata], [onProgress])

        • data - Any value to send (primitive, object, binary). Serializationand chunking is handled automatically. Binary data (e.g.Blob,TypedArray) is received by other peer as an agnosticArrayBuffer.

        • targetPeers -(optional) Either a peer ID (string), an array ofpeer IDs, ornull (indicating to send to all peers in the room).

        • metadata -(optional) If the data is binary, you can send anoptional metadata object describing it (seeBinary metadata).

        • onProgress -(optional) A callback function that will be calledas every chunk for every peer is transmitted. The function will becalled with a value between 0 and 1 and a peer ID. SeeProgress updates for an example.

    2. Receiver

      • Registers a callback function that runs when data for this action isreceived from other peers.

      • (data, peerId, metadata)

        • data - The value transmitted by the sending peer. Deserialization ishandled automatically, i.e. a number will be received as a number, anobject as an object, etc.

        • peerId - The ID string of the sending peer.

        • metadata -(optional) Optional metadata object supplied by thesender ifdata is binary, e.g. a filename.

    3. Progress handler

      • Registers a callback function that runs when partial data is receivedfrom peers. You can use this for tracking large binary transfers. SeeProgress updates for an example.

      • (percent, peerId, metadata)

        • percent - A number between 0 and 1 indicating the percentage completeof the transfer.

        • peerId - The ID string of the sending peer.

        • metadata -(optional) Optional metadata object supplied by thesender.

    Example:

    const[sendCursor,getCursor]=room.makeAction('cursormove')window.addEventListener('mousemove',e=>sendCursor([e.clientX,e.clientY]))getCursor(([x,y],peerId)=>{constpeerCursor=cursorMap[peerId]peerCursor.style.left=x+'px'peerCursor.style.top=y+'px'})
  • ping(peerId)

    Takes a peer ID and returns a promise that resolves to the milliseconds theround-trip to that peer took. Use this for measuring latency.

    • peerId - Peer ID string of the target peer.

    Example:

    // log round-trip time every 2 secondsroom.onPeerJoin(peerId=>setInterval(async()=>console.log(`took${awaitroom.ping(peerId)}ms`),2000))

selfId

A unique ID string other peers will know the local user as globally acrossrooms.

getRelaySockets()

(🌊 BitTorrent, 🐦 Nostr, 📡 MQTT only) Returns an object of relay URL keysmapped to their WebSocket connections. This can be useful for determining thestate of the user's connection to the relays and handling any connectionfailures.

Example:

console.log(trystero.getRelaySockets())// => Object {//  "wss://tracker.webtorrent.dev": WebSocket,//  "wss://tracker.openwebtorrent.com": WebSocket//  }

getOccupants(config, roomId)

(🔥 Firebase only) Returns a promise that resolves to a list of user IDspresent in the given namespace. This is useful for checking how many users arein a room without joining it.

  • config - A configuration object
  • roomId - A namespace string that you'd pass tojoinRoom().

Example:

console.log((awaittrystero.getOccupants(config,'the_scope')).length)// => 3

Strategy comparison

one-time setup¹bundle size²
🐦Nostrnone 🏆16K
📡MQTTnone 🏆75K
🌊BitTorrentnone 🏆5K 🏆
⚡️Supabase~5 mins27K
🔥Firebase~5 mins43K
🪐IPFSnone 🏆143K

¹ All strategies except Supabase and Firebase require zero setup. Supabaseand Firebase are managed strategies which require setting up an account.

² Calculated via Terser minification + Brotli compression.

How to choose

Trysteroʼs unique advantage is that it requires zero backend setup and usesdecentralized infrastructure in most cases. This allows for frictionlessexperimentation and no single point of failure. One potential drawback is thatitʼs difficult to guarantee that the public infrastructure it uses will alwaysbe highly available, even with the redundancy techniques Trystero uses. Whilethe other strategies are decentralized, the Supabase and Firebase strategies area more managed approach with greater control and an SLA, which might be moreappropriate for “production” apps.

Trystero makes it trivial to switch between strategies — just change a singleimport line and quickly experiment:

import{joinRoom}from'trystero/[torrent|nostr|mqtt|supabase|firebase|ipfs]'

Trystero byDan Motzenbecker


[8]ページ先頭

©2009-2025 Movatter.jp