GithubHelp home page GithubHelp logo

dmotz / trystero Goto Github PK

View Code? Open in Web Editor NEW
877.0 13.0 70.0 955 KB

🀝 Build instant multiplayer webapps, no server required β€” Magic WebRTC matchmaking over BitTorrent, Nostr, MQTT, IPFS, and Firebase

Home Page: https://oxism.com/trystero

License: MIT License

JavaScript 99.77% HTML 0.23%
webrtc p2p peer-to-peer chat pairing matchmaking ipfs bittorrent webtorrent serverless

trystero's Introduction

🀝 Trystero

Build instant multiplayer webapps, no server required

πŸ‘‰ TRY THE DEMO πŸ‘ˆ

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

Peers can connect via 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT, πŸ”₯ Firebase, or πŸͺ IPFS – all using the same API.

Besides making peer matching automatic, Trystero offers some nice abstractions on 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

Contents


How it works

πŸ‘‰ If you just want to try out Trystero, you can skip this explainer and jump into using it.

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

The important point to remember is this:

πŸ”’

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

πŸ‘†

You can compare 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 the latest release and import it locally:

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

By default, the BitTorrent strategy is used. To use a different one just deep import like so (your bundler should handle including only relevant code):

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

Next, join the user to a room with a namespace:

const config = {appId: 'san_narciso_3d'}
const room = joinRoom(config, 'yoyodyne')

The first argument is a configuration object that requires an appId. This should be a completely unique identifier for your app (or in the case of Firebase, your databaseURL). The second argument is the room name.

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

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()

Broadcast events

Send peers your video stream:

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

Send and subscribe to custom P2P actions:

const [sendDrink, getDrink] = room.makeAction('drink')

// buy drink for a friend
sendDrink({drink: 'negroni', withIce: true}, friendId)

// buy round for the house (second argument omitted)
sendDrink({drink: 'mezcal', withIce: false})

// listen for drinks sent to you
getDrink((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 TypedArray
canvas.toBlob(blob => sendPic(blob))

// binary data is received as raw ArrayBuffers so your handling code should
// interpret it in a way that makes sense
getPic(
  (data, peerId) => (imgs[peerId].src = URL.createObjectURL(new Blob([data])))
)

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

const idsToNames = {}
const [sendName, getName] = room.makeAction('name')

// tell other peers currently in the room our name
sendName('Oedipa')

// tell newcomers
room.onPeerJoin(peerId => sendName('Oedipa', peerId))

// listen for peers naming themselves
getName((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 the scenes. This means you can send very large files and whatever data you send will 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 later
const peerAudios = {}

// get a local audio stream from the microphone
const selfStream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: false
})

// send stream to peers currently in the room
room.addStream(selfStream)

// send stream to peers who join later
room.onPeerJoin(peerId => room.addStream(selfStream, peerId))

// handle streams from other peers
room.onPeerStream((stream, peerId) => {
  // create an audio instance and set the incoming stream
  const audio = new Audio()
  audio.srcObject = stream
  audio.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 to video elements in the DOM:

const peerVideos = {}
const videoContainer = document.getElementById('videos')

room.onPeerStream((stream, peerId) => {
  let video = peerVideos[peerId]

  // if this peer hasn't sent a stream before, create a video element
  if (!video) {
    video = document.createElement('video')
    video.autoplay = true

    // add video element to the DOM
    videoContainer.appendChild(video)
  }

  video.srcObject = stream
  peerVideos[peerId] = video
})

Advanced

Binary metadata

Let's say your app supports sending various types of files and you want to annotate the raw bytes being sent with metadata about how they should be interpreted. Instead of manually adding metadata bytes to the buffer you can simply 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 null
sendFile(buffer, null, {name: 'The CourierΚΌs Tragedy', type: 'application/pdf'})

Action promises

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

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

Progress updates

Action sender functions also take an optional callback function that will be continuously called as the transmission progresses. This can be used for showing a progress bar to the sender for large tranfers. The callback is called with a percentage 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 the receiving user that there is a transfer in progress with perhaps the name of the incoming file.

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

Encryption

Once peers are connected to each other all of their communications are end-to-end encrypted. During the initial connection / discovery process, peers' SDPs are sent via the chosen peering strategy medium. The SDP is encrypted over the wire, but is visible in plaintext as it passes through the medium (a public torrent tracker for example). This is fine for most use cases but you can choose to hide SDPs from the peering medium with Trystero's encryption option. This can protect against a MITM peering attack if both intended peers have a shared secret. To opt in, just pass a password parameter in the app configuration object:

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

Keep in mind the password has to match for all peers in the room for them to be able to connect. An example use case might be a private chat room where users learn the password via external means.

React hooks

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

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

import {joinRoom} from 'trystero'
import {useState} from 'react'

const trysteroConfig = {appId: 'thurn-und-taxis'}

export default function App({roomId}) {
  const room = joinRoom(trysteroConfig, roomId)
  const [sendColor, getColor] = room.makeAction('color')
  const [myColor, setMyColor] = useState('#c0ffee')
  const [peerColors, setPeerColors] = useState({})

  // whenever a new peer joins, send my color to them
  room.onPeerJoin(peer => sendColor(myColor, peer))

  getColor((color, peer) =>
    setPeerColors(peerColors => ({...peerColors, [peer]: color}))
  )

  const updateColor = e => {
    const {value} = e.target

    setMyColor(value)
    // when updating my own color, broadcast it to all peers
    sendColor(value)
  }

  return (
    <>
      <h1>Trystero + React</h1>

      <h2>My color:</h2>
      <input type="color" value={myColor} onChange={updateColor} />

      <h2>Peer colors:</h2>
      <ul>
        {Object.entries(peerColors).map(([peerId, color]) => (
          <li key={peerId} style={{backgroundColor: color}}>
            {peerId}: {color}
          </li>
        ))}
      </ul>
    </>
  )
}

Astute readers may notice the above example is simple and doesn't consider if we want to change the component's room ID or unmount it. For those scenarios you can use this simple useRoom() hook that unsubscribes from room events accordingly:

import {joinRoom} from 'trystero'
import {useEffect, useRef} from 'react'

export const useRoom = (roomConfig, roomId) => {
  const roomRef = useRef(joinRoom(roomConfig, roomId))

  useEffect(() => {
    roomRef.current = joinRoom(roomConfig, roomId)
    return () => roomRef.current.leave()
  }, [roomConfig, roomId])

  return roomRef.current
}

Firebase setup

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

  1. Create a Firebase project
  2. Create a new Realtime Database
  3. Copy the databaseURL and use it as the appId in your Trystero config
  4. [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 is known ahead of time.

API

joinRoom(config, namespace)

Adds local user to room whereby other peers in the same namespace will open communication channels and send events. Calling joinRoom() multiple times with the same namespace will return the same room instance.

  • config - Configuration object containing the following keys:

    • appId - (required) A unique string identifying your app. If using Firebase, this should be the databaseURL from your Firebase config (also see firebaseApp below for an alternative way of configuring the Firebase strategy).

    • password - (optional) A string to encrypt session descriptions as they are passed through the peering medium. If set, session descriptions will be encrypted using AES-CBC. The password must match between any peers in the namespace for them to connect. Your site must be served over HTTPS for the crypto module to be used. See encryption for more details.

    • rtcConfig - (optional) Specifies a custom RTCConfiguration for all peer connections.

    • relayUrls - (optional, 🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT only) Custom list of URLs for the strategy to use to bootstrap P2P connections. These would 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 in case some fail. Passing a relayUrls option will cause this option to be ignored as the entire list will be used.

    • firebaseApp - (optional, πŸ”₯ Firebase only) You can pass an already initialized Firebase app instance instead of an appId. Normally Trystero will initialize a Firebase app based on the appId but this will fail if youΚΌve already initialized it for use elsewhere.

    • rootPath - (optional, πŸ”₯ Firebase only) String specifying path where Trystero writes its matchmaking data in your database ('__trystero__' by default). Changing this is useful if you want to run multiple apps using the same database and don't want to worry about namespace collisions.

    • libp2pConfig - (optional, πŸͺ IPFS only) Libp2pOptions where you can specify a list of static peers for bootstrapping.

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

Returns an object with the following methods:

  • leave()

    Remove local user from room and unsubscribe from room events.

  • getPeers()

    Returns a map of RTCPeerConnections for the peers present in room (not including the local user). The keys of this object are the respective peers' IDs.

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

    Broadcasts media stream to other peers.

    • stream - A MediaStream with audio and/or video to send to peers in the room.

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

    • metadata - (optional) Additional metadata (any serializable type) to be sent with the stream. This is useful when sending multiple streams so recipients know which is which (e.g. a webcam versus a screen capture). If you want to broadcast a stream to all peers in the room with a metadata argument, pass null as the second argument.

  • removeStream(stream, [targetPeers])

    Stops sending previously sent media stream to other peers.

    • stream - A previously sent MediaStream to stop sending.

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

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

    Adds a new media track to a stream.

    • track - A MediaStreamTrack to add to an existing stream.

    • stream - The target MediaStream to attach the new track to.

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

    • metadata - (optional) Additional metadata (any serializable type) to be sent with the track. See metadata notes for addStream() above for more details.

  • removeTrack(track, stream, [targetPeers])

    Removes a media track from a stream.

    • track - The MediaStreamTrack to remove.

    • stream - The MediaStream the track is attached to.

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

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

    Replaces a media track with a new one.

    • oldTrack - The MediaStreamTrack to remove.

    • newTrack - A MediaStreamTrack to attach.

    • stream - The MediaStream the oldTrack is attached to.

    • targetPeers - (optional) If specified, the track is replaced only for the 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 the peer'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 the peer's ID.

    Example:

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

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

    • callback(stream, peerId, metadata) - Function to run whenever a peer sends a media stream, called with the the peer's stream, ID, and optional metadata (see addStream() 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 media track. If called more than once, only the latest callback registered is ever called.

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

    Example:

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

    Listen for and send custom data actions.

    • namespace - 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 all target peers are finished receiving data.

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

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

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

        • metadata - (optional) If the data is binary, you can send an optional metadata object describing it (see Binary metadata).

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

    2. Receiver

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

      • (data, peerId, metadata)

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

        • peerId - The ID string of the sending peer.

        • metadata - (optional) Optional metadata object supplied by the sender if data is binary, e.g. a filename.

    3. Progress handler

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

      • (percent, peerId, metadata)

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

        • peerId - The ID string of the sending peer.

        • metadata - (optional) Optional metadata object supplied by the sender.

    Example:

    const [sendCursor, getCursor] = room.makeAction('cursormove')
    
    window.addEventListener('mousemove', e => sendCursor([e.clientX, e.clientY]))
    
    getCursor(([x, y], peerId) => {
      const peerCursor = 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 the round-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 seconds
    room.onPeerJoin(peerId =>
      setInterval(
        async () => console.log(`took ${await room.ping(peerId)}ms`),
        2000
      )
    )

selfId

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

getRelaySockets()

(🌊 BitTorrent, 🐦 Nostr, πŸ“‘ MQTT only) Returns an object of relay URL keys mapped to their WebSocket connections. This can be useful for determining the state of the user's connection to the relays and handling any connection failures.

Example:

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

getOccupants(config, namespace)

(πŸ”₯ Firebase only) Returns a promise that resolves to a list of user IDs present in the given namespace. This is useful for checking how many users are in a room without joining it.

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

Example:

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

Strategy comparison

one-time setupΒΉ bundle sizeΒ² time to connectΒ³
🌊 BitTorrent none πŸ† 25K πŸ† ⏱️⏱️
🐦 Nostr none πŸ† 54K ⏱️⏱️
πŸ“‘ MQTT none πŸ† 332K ⏱️⏱️
πŸ”₯ Firebase ~5 mins 177K ⏱️ πŸ†
πŸͺ IPFS none πŸ† 1MB ⏱️⏱️⏱️

ΒΉ All strategies except Firebase require zero setup. Firebase is a managed strategy which requires setting up an account.

Β² Calculated via Rollup bundling + Terser compression.

Β³ Relative speed of peers connecting to each other when joining a room. Firebase is near-instantaneous while the other strategies are a bit slower to exchange peering info.

How to choose

TrysteroΚΌs unique advantage is that it requires zero backend setup and uses decentralized infrastructure in most cases. This allows for frictionless experimentation and no single point of failure. One potential drawback is that itΚΌs difficult to guarantee that the public infrastructure it uses will always be highly available, even with the redundancy techniques Trystero uses. While the other strategies are decentralized, the Firebase strategy is a more managed approach with greater control and an SLA, which might be more appropriate for β€œproduction” apps.

Luckily, Trystero makes it trivial to switch between strategies β€” just change a single import line and quickly experiment:

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

Trystero by Dan Motzenbecker

trystero's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

trystero's Issues

Unable to import trystero

hi, Please help me with an issue.

The following error is raised when i try to import trystero

Framework: sveltekit + vite

(node:29562) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
6:24:07 pm [vite] Error when evaluating SSR module /src/routes/index.js: failed to import "trystero"
|- /home/kar/my/projects/mayasabha/node_modules/simple-peer-light/index.js:1171
export default Peer
^^^^^^

SyntaxError: Unexpected token 'export'
    at internalCompileFunction (node:internal/vm:73:18)
    at wrapSafe (node:internal/modules/cjs/loader:1178:20)
    at Module._compile (node:internal/modules/cjs/loader:1220:27)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:169:29)
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)

6:24:07 pm [vite] Error when evaluating SSR module /src/routes/+page.svelte: failed to import "/src/routes/index.js"
|- /home/kar/my/projects/mayasabha/node_modules/simple-peer-light/index.js:1171
export default Peer
^^^^^^

SyntaxError: Unexpected token 'export'
    at internalCompileFunction (node:internal/vm:73:18)
    at wrapSafe (node:internal/modules/cjs/loader:1178:20)
    at Module._compile (node:internal/modules/cjs/loader:1220:27)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:169:29)
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)

/home/kar/my/projects/mayasabha/node_modules/simple-peer-light/index.js:1171
export default Peer
^^^^^^

SyntaxError: Unexpected token 'export'
    at internalCompileFunction (node:internal/vm:73:18)
    at wrapSafe (node:internal/modules/cjs/loader:1178:20)
    at Module._compile (node:internal/modules/cjs/loader:1220:27)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:169:29)
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)
/home/kar/my/projects/mayasabha/node_modules/simple-peer-light/index.js:1171
export default Peer
^^^^^^

SyntaxError: Unexpected token 'export'
    at internalCompileFunction (node:internal/vm:73:18)
    at wrapSafe (node:internal/modules/cjs/loader:1178:20)
    at Module._compile (node:internal/modules/cjs/loader:1220:27)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:169:29)
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)

Node.js v18.18.0

Customizable requirements to join room

Hi, first of all great library & great docs, I'm using it as a core component of a complex webapp I'm building and I'm very satisfied πŸ‘

I was thinking, would it be difficult to implement customizable requirements to join the room?

Like, an option that can take a function as value, and that function must evaluates to true to join; maybe another option that takes a custom form in object/json format, so that the function can use its values as args.

I'm thinking of using PGP under the hood for P2P user verification, but it seems to me that atm I could only use it to flag users as impostors . The options I proposed would make it super easy to filter people at the user level & stopping them from joining the room altogether (it would be useless against someone who's tampering with the code tho)

Would it be difficult to implement it with the current structure of the library?

[feature request] Demos for all connection strategies

Hey there! Thanks for sharing this awesome library. It's one of the coolest projects I've seen recently! I've only experimented with it so far but I'm evaluating it for use in a game I'm developing.

I'd prefer to use the BitTorrent strategy because my heart yearns for decentralization. πŸ˜‰ The official demo only uses the Firebase strategy, and it would be great if the alternative strategies could be toggled between to see how their behavior compares. So far I've just been hacking docs/site.js file to do this. Having this functionality on the demo site could also serve as a reference implementation for others who might want to use a decentralized strategy.

Thanks again for sharing the awesome work!

Session descriptor signing and verification support

Hi! I just found this project recently and it seems super useful. The symmetric encryption scheme mentioned in https://github.com/dmotz/trystero#encryption looks very nice for preventing MITM attacks when the room password is private. However, I'm looking into building a system with public rooms, so the symmetric encryption is not viable for me. What I'd like is a scheme where each peer generated a public/private keypair and uses that to encrypt the SDPs, like what SaltyRTC does except decentralized (https://saltyrtc.org).

When a new peer B is found, peer A would need to encrypt it's SDP offer with peer B's public key. Peer B would then decrypt the SDP, make an answer and encrypt it with peer A's public key, sending it back. These public keys could either be gossipped or known in advance.

How possible is it for something like this to be implemented? I don't know a lot about how the BitTorrent tracker websocket works. Would it allow sending messages like this?

Edit: I'm re-opening this as support for session descriptor signing. See #19 (comment).

Custom IPFS Gateway

We are currently looking into using Trystero for our decentralised gaming client.

Is it possible to use our custom IPFS gateway instead of a public one to guarantee uptime?

Re-connect

I'm getting some re-connection difficulties.

  1. Two clients connect (one is a phone).
  2. Phone display is turned off.
  3. 2 minutes later the connection is broken and the other browser shows this error in console.
crypto-254fb80e.js:232 Error: Connection failed.
    at Peer._onConnectionStateChange (simple-peer-light.js:550:28)
    at Peer._pc.onconnectionstatechange (simple-peer-light.js:105:12)

I attempted various combinations of .leave() and .joinRoom without luck.

Demo is not working for me

For some reason, in the demo nothing happens for me : I simultaneously opened it on 2 computers (both Linux OS), on multiple different browsers (Firefox, Chromium, Brave) many tabs, turned off extensions and Firefox protections, and still nothing. In each tab my arrow is alone, next to the text "Try it, I dare you" :(
(I can move my arrow around, click and drop fruits, though)

I wonder what is wrong with my computers/browsers/etc. ?

Firebase - Service database is not available

When trying to intialise a Firebase setup:

component.js:111 Uncaught Error: Service database is not available
    at Provider.getImmediate (component.js:111:15)
    at getDatabase (database.js:9780:45)
    at init (firebase.js:18:34)
    at firebase.js:21:14
    at crypto-36a3f9c0.js:30:10

I tried using the app ID, as well as the realtime database url, but to no avail.

According to this stack overflow it may be because the version of the Firebase SDK this project uses is out of date?

https://stackoverflow.com/questions/70823077/uncaught-error-service-database-is-not-available-firebase-javascript

question: project status

Hi,

thanks for your amazing work on trystero. I am trying to build a project that will synchronize data of one or few users on multiple devices.
I've played around with webRTC, but I've always been put off by the need to run servers that kind of defeat the idea of p2p communication.

I tried syncing across multiple devices today and it works perfectly. Is this project production ready?

I would like to create a few PRs if it is okay with you:

  • there are no tests, but at least some utils functions can be easily tested
  • project has d.ts files but is written in JavaScript. Do you mind if I rewrite project to TypeScript?
  • current version works only on local network. I think it can be fixed by adding STUN servers. There are free public STUN servers which can be used in default configuration same way as you added default torrent trackers.

Node.js support

Support for Trystero running in a Node.js environment could enable some really interesting use cases. @dmotz would you be interested in supporting Node? If so, then I'd be happy to work on a PR to explore this!

Uncaught TypeError: crypto.subtle is undefined

Hi ,
before anything else let me tell you how awesome found this project!
For sure deserves more attention than currently has...

To the issue now
Steps followed :

  • Cloned repo
  • runned locally ( with serve npm module)

So no further peer connection utilized after that i suppose.

image

Callbacks and leave() seem unreliable?

Callbacks like onPeerLeave and onPeerJoin dont seem to be firing reliably? Especially onPeerLeave. Is this a known bug or issue?

I also notice that sometimes when trying to move a peer from one room to another, by calling room.leave() on the current room and then joinRoom() for the new one, that sometimes the user get stuck in both and continues to receive messages/data from both? To the point that even after calling leave() on a room, if other peers do a room.getPeers() call, it will continue to show the peer that should have been removed?

Example Complement

Hi! I tried following your example to create a voice chat, but it doesn't work. Peers are connected, but there is no audio. Should I add a track? How it works?

import {joinRoom} from 'trystero';

const config = {appId: 'test08658'};

const room = joinRoom(config, 'vox');

room.addStream(navigator.mediaDevices.getUserMedia({audio: true, video: false}));

WebRTC: ICE failed

Hi,
Thanks a lot for making this abstract handy library !!!

As for the results, I already had it running without problems, then all of a sudden I'm having:

WebRTC: ICE failed, add a TURN server and see about:webrtc for more details

So when I check I have the followings:

image

I don't know anything about bittorrent network, any help is so much appreciated !!

Edit:
All I'm doing is the basic example of one server joinRoom and same for clients. I'm using client sessions (chrome, firefox)

Thanks a lot

support Hypercore

any plans to support hypercore? there is hyperswarm-web that you can use.

Reconnecting doesn't work properly with Firebase

Repro steps:

  • User 1 creates a room with a hardcoded app ID and room ID
  • User 2 joins the room
  • User 2 calls room.leave()
  • User 2 attempts to join the room again, without refreshing. Same app ID, room ID, and peer ID.
  • The onPeerJoin function does not execute for User 1.
  • If I switch the backend to bittorrent, it works fine. It also works fine on a refresh when a new peer ID is generated.

I'm guessing this has something to do with entries in connectedPeers or peerMap not being properly evicted on a disconnect, but I haven't had a chance to familiarize myself with the Firebase API or everything trystero/firebase.js is doing.

WebRTC connection takes 5~10 seconds and multiple ICE TURN servers fail

Last week my code did work but currently multiple WebRTC signalling server seems to be broken. Which means I can messages send to other peers arrive with a 10 second delay. I assume it may be related to the simple-peer-light WebRTC dependency.

Code:

const createRoom = (roomConfig: BaseRoomConfig, roomId: string) => {
  return joinRoom(roomConfig, roomId);
};
const roomConfig = {
  appId: '<Your APP ID here>',
};
const roomId = <Your ROOM ID here>;
const room = createRoom(roomConfig, roomId);
room.onPeerJoin(peerId => console.log(`${peerId} joined`));

Reproducible example:

  1. Create a room.
  2. Let 1 user join the room.
  3. Let another user join the same room.

Expected behaviour:
Users in the same room get an update of the joined users.

Current behaviour:
ICE Turn Server failed. Multiple ICE TURN servers seem to fail until it finally succeeds with a backup server.
image
image

Extra details

I've tried different internet networks (Netherlands - EU):

  • home network
  • work network
  • 4G network

Room providers:

  • IPFS
  • BitTorrent
  • FireBase

trystero version

  • 0.16.0

OS:

  • MacOS 14.0

Browsers:

  • Edge
  • Safari
  • LibreWolf (hardened FireFox)
  • Chrome

P2PT as a fallback for Trystero? Or does Trystero already do exact the same?

Does Trystero basically what P2PT does as well? It creates a torrent based on a 'topic' and peers interested in that topic swarm around that torrent and connect to each other?

If not, whats the difference? How exactly does Trystero do it?

I like to use P2PT as a fallback for Trystero, but maybe Trystero does already, was P2PT does?

Every client is also a tracker itself, so its able to fetch connection adresses (DHT)

  1. direct (webrtc) and indirect (torrents, IPFS) (webrtc prioritized, Trystero reconnects through torrents and ipfs as a fallback)
  2. indirect/fallback (torrent/magnetlink) (P2PT) reconnect, than back to 1. (direct)

For a project about private/direct/secure comms, why's it so impossible to self host?

I have tried a bazillion ways to get the cloned repo to perform as the CDN does. Or to wget all the CDN files and host them locally. Why is it such a convoluted series of imports from a remote server?

Why do the instructions omit how to host locally?

If the project is about private and secure comms, wouldn't the ability to host the code yourself be at the forefront?

[Question]: Detecting incorrect passwords

I'm using joinRoom from trystero/firebase , but I'm having trouble detecting if the user's inputted password successfully connected them to the room. It seems that the library throws this error internally when failing to connect: firebase.js:122 Trystero: received malformed SDP JSON . But I'm not sure how to reliably detect that.

Uncaught Error: Service database is not available

I have a confusing issue - currently I have trystero working with all services but firebase.

<script type="module">import {joinRoom} from 'https://cdn.skypack.dev/trystero/firebase'; 

    const config = {appId: '*****-*****-default-rtdb'} // starred out exact database instance
    const room = joinRoom(config, 'yoyodyne')
    room.onPeerJoin(peerId => console.log(`${peerId} joined`))
    room.onPeerLeave(peerId => console.log(`${peerId} left`))
 
// this object can store audio instances for later
const peerAudios = {}

// get a local audio stream from the microphone
const selfStream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: false
}).catch(function(err) {
    //log to console first 
    console.log(err); /* handle the error */
    if (err.name == "NotFoundError" || err.name == "DevicesNotFoundError") {
        //required track is missing 
    } else if (err.name == "NotReadableError" || err.name == "TrackStartError") {
        //webcam or mic are already in use 
    } else if (err.name == "OverconstrainedError" || err.name == "ConstraintNotSatisfiedError") {
        //constraints can not be satisfied by avb. devices 
    } else if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") {
        //permission denied in browser 
    } else if (err.name == "TypeError" || err.name == "TypeError") {
        //empty constraints object 
    } else {
        //other errors 
    }
});

// send stream to peers currently in the room
room.addStream(selfStream)

// send stream to peers who join later
//room.onPeerJoin(peerId => room.addStream(selfStream, peerId))

// handle streams from other peers n

room.onPeerStream((stream, peerId) => {
  // create an audio instance and set the incoming stream
  const audio = new Audio()
  audio.srcObject = stream
  audio.autoplay = true

  // add the audio to peerAudio object if you want to address it for something
  // later (volume, etc.)
  peerAudios[peerId] = audio
})
</script>

I'm getting the following error:

Uncaught Error: Service database is not available
    getImmediate https://cdn.skypack.dev/-/@firebase/[email protected]/dist=es2019,mode=imports/optimized/@firebase/component.js:111
    getDatabase https://cdn.skypack.dev/-/@firebase/[email protected]/dist=es2019,mode=imports/optimized/@firebase/database.js:9780
    init https://cdn.skypack.dev/-/[email protected]/dist=es2019,mode=imports/optimized/trystero/firebase.js:18
    joinRoom https://cdn.skypack.dev/-/[email protected]/dist=es2019,mode=imports/optimized/trystero/firebase.js:21
    initGuard https://cdn.skypack.dev/-/[email protected]/dist=es2019,mode=imports/optimized/common/crypto-79d472b6.js:30
    <anonymous> https://buysomeonecoffee.com/:200
[component.js:111:15](https://cdn.skypack.dev/-/@firebase/[email protected]/dist=es2019,mode=imports/optimized/@firebase/component.js)

My firebase database rules are:

{
  "rules": {
    ".read": false,
    ".write": false,
    "__trystero__": {
      ".read": false,
      ".write": false,
      "$room_id": {
        ".read": true,
        ".write": true
      }
    }
  }
}

There was some suggestion on StackOverflow similar issues could be due to incompatible versions of Firebase. I just created my Firebase instance today, so it should not be outdated.

Any suggestions? Appreciated very frustrating issue as Firebase is the most reliable WebRTC connector and it's simply not working for me.

non web dev version

Is there a possibility to use the same technique to for example a native app in android or other operating system?
I know that node js could be encapsulated in an app or run alongside as a relay but Im more into tightly integrating it into one app code.

What is needed to self-host with custom BT trackers?

Trystero looks amazing. I'm evaluating it as a solution for p2p "rooms" that span different blockchain nodes in a new blockchain network.

Suppose every node in my blockchain network would be hosting a torrent tracker, can Trystero be configured to use these as opposed to the hardcoded public ones?

maximum connections, full mesh vs partial mesh

what is the maximum connections we can have?

webrtc will not be able to handle too many connections at the same time since you have to connect to each peer separately, lots of usage of resource.

there is "partial mesh" where you forward data to your peers, so if you are indirectly connected with a common peer, then the data and messages still gets sent across the network.

like this https://github.com/lazorfuzz/liowebrtc or https://github.com/geut/discovery-swarm-webrtc

any thoughts on this or scalability? do we know around how many connections trystero can do, is there any maximum limit we know of with trystero?

room.onPeerStream not firing

Hi @dmotz ,

When I room.addStream on one peer, I do not see the onPeerStream firing.

room = joinRoom(config, ns);
[sendAudio, getAudio] = room.makeAction("audio");
room.onPeerStream((stream, id) => console.log(`got stream from ${id}`, stream));

That's how I set the room up. room.onPeerStream doesn't fire for me.

Click function that is supposed to create stream below

      talking = await navigator.mediaDevices.getUserMedia({audio: true, video: false});
      console.log('ship out',talking)
      room.addStream(talking,selfId);

Add stream is called, in one peer, but the other does not see the stream or add the stream

Can all peers see each others IP addresses?

Just wondering how secure/anonymous trystero is.

If I create a room and a number of users join, can they all see the IP addresses of the other people? And if so how easy is that to access?

Reconnection Logic

Wonderful little library you're building here!

I was playing with the demo and comparing it to our DAM/GUN approach using the bittorrent strategy, and while everything seems to work as advertised once the mesh is established, I noticed the bittorrent websocket connections are quite fragile and close quite quickly, without an obvious reconnection logic to restart announceAll and staying advertised to the trackers

WebSocket is already in CLOSING or CLOSED state.
announce @ torrent.js:102

The immediate effects is peers are not discovered when they join the page far apart in time. Did i miss anything obvious for those scenarios and/or can one be discussed to form a code contribution?

Thanks!

stripped zoom example

Hi,

First of all: thanks!

Thanks for leading us out of the dark ages of p2p/webrtc which require plumbing a signalling-server together.
This has really been holding back the p2p movement in a certain sense (people don't like too much moving parts).
With all the common good online infrastructure (bittorrent e.g.), a signaling 'commons' was really something I was hoping for (but didn't had the skills to develop something like this).

Anyways, it works like a charm.
I've re-created the README.md example-snippets into a stripped zoom-client:

https://gist.github.com/coderofsalvation/b70d635061220f2860f21eb69c8e5918

Feel free to use it.

Idea: put it in the repo as example/zoom.html and add the following .github/workflows/website.yml:

# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ["main"]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  # Single deploy job since we're just deploying
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Pages
        uses: actions/configure-pages@v3
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v1
        with:
          path: '.'
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2
  • activate github pages in settings
  • Optionally put an index.html landingpage in the root of the repo

Boom..you have a project/demos which people can check out with one click

Binary/Blob truncation on receiver

The "binary" pipeline example using makeAction appears to be only presenting a chunk of actual data to the get client:

const [sendPic, getPic] = room.makeAction('pic')

// blobs are automatically handled, as are any form of TypedArray
canvas.toBlob(blob => sendPic(blob))

// binary data is received as raw ArrayBuffers so your handling code should
// interpret it in a way that makes sense
getPic((data, id) => (imgs[id].src = URL.createObjectURL(new Blob([data]))))

This seems easy to reproduce by sending anything (image or else) larger than a few KBs over to see it truncated using the bittorrent strategy room. Interestingly enough, the overall blob size on the receiver seems correct, but filled with 0/null values past the first valid chunk.

Did anyone succeed in transmitting full Blobs with the above (and/or any variation)?

Thanks in advance!

[Question] `trackerRedundancy` maximum of 3

Hi, per the documentation for trackerRedundancy:

Integer specifying how many torrent trackers to connect to simultaneously in case some fail. Defaults to 2, maximum of 3.

However, I'm not seeing this maximum limit reflected in the code. Here's the only place in the code where trackerRedundancy seems to be read:

trystero/src/torrent.js

Lines 39 to 44 in 1602156

const trackerUrls = (config.trackerUrls || defaultTrackerUrls).slice(
0,
config.trackerUrls
? config.trackerUrls.length
: config.trackerRedundancy || defaultRedundancy
)

Is the maximum of 3 enforced in some less obvious way? If not, is there any reason one couldn't set trackerRedundancy to a higher number?

TypeScript Question: How to use room.makeAction when room is possible null?

Hi there I am using the custom useRoom hook from the readme example to create a room. I modified it slightly to work with Next.JS SSR.

'use client';

import { type BaseRoomConfig, joinRoom } from 'trystero';
import { useCallback, useEffect, useRef } from 'react';
import { useAppDispatch } from '../redux/store';
import { updatePokerRoomId } from '../redux/feature/poker-room/pokerRoomSlice';

export const useMultiplayerRoom = (
  roomConfig: BaseRoomConfig,
  roomId: string
) => {
  const createRoom = useCallback(
    (roomConfig: BaseRoomConfig, roomId: string) => {
      if (typeof window !== 'undefined') {
        return joinRoom(roomConfig, roomId);
      }
    }, []
  );

  const roomRef = useRef(createRoom(roomConfig, roomId));

  useEffect(() => {
    if (typeof window !== 'undefined') {
      roomRef.current = createRoom(roomConfig, roomId);
      return () => {
        if (roomRef.current) {
          roomRef.current.leave();
        }
      };
    }
  }, [createRoom, roomConfig, roomId, setupMultiplayerRoomConnection]);

  return roomRef.current;
};

TypeScript gives me an error saying the room may be possibly null when I try to make an action using the room connection

 const [sendUser, getUser] = multiplayerPokerRoomConnection?.makeAction('user');

Error: Type '[ActionSender<unknown>, ActionReceiver<unknown>, ActionProgress] | undefined' is not an array type.

Any tips for solving this error? Does makeAction always need to be called as a hook or can I just check for the existence of a room in a function and register the event listener once in a function?

Vulnerability warnings about `ipfs-core` dependency

Hi πŸ‘‹ Thanks for creating this lib!

After installing it with NPM, I've got a lot of warnings due to vulnerabilities found on sub-dependencies of [email protected].

As I'm not using the ipfs strategy, I've set an override for it as a workaround and confirmed that using the latest version ([email protected]) resolves all the warnings.

"overrides": {
  "ipfs-core": "latest"
}

Could you update it in the lib dependencies, so we don't need the workaround anymore?

"ipfs-core": "0.9.0",

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.