🌟 Sparrow makes WebRTC easy.
🚀 Try the demo at https://sparrow.benev.gg/
🤝 WebRTC is peer-to-peer networking between browser tabs.
🎮 Perfect for making player-hosted multiplayer web games.
💪 Self-hostable, read self-hosting.md for instructions.
💖 Free and open source.
The Objective:
Connect two players together, and establish RTC Data Channels between them.
- Install
sparrow-rtcnpm i sparrow-rtc
- Host a session
import Sparrow from "sparrow-rtc" const sparrow = await Sparrow.host({ // accept people joining, send/receive some data welcome: prospect => connection => { console.log(`peer connected: ${connection.id}`) connection.cable.reliable.send("hello") connection.cable.reliable.onmessage = m => console.log("received", m.data) return () => console.log(`peer disconnected: ${connection.id}`) }, // lost connection to the sparrow signaller closed: () => console.warn(`connection to sparrow signaller has died`), }) // anybody with this invite code can join sparrow.invite // "215fe776f758bc44"
- Join that session
import Sparrow from "sparrow-rtc" const sparrow = await Sparrow.join({ invite: "215fe776f758bc44", disconnected: () => console.log(`disconnected from host`), }) // send and receive data sparrow.connection.cable.reliable.send("world") sparrow.connection.cable.reliable.onmessage = m => console.log("received", m.data)
- Shut it down
- You can close each peer connection like this:
connection.disconnect()
- You can close the connection to the signaller server like this:
sparrow.close()
- These operations can be done independently from each other.
- If you close the signaller connection, you can still maintain your peer connections.
- When the signaller connection is closed, nobody can join you anymore because the signaller is required to negotiate connections.
- You can close each peer connection like this:
- The
cableis what you want. The cable is what you need. - Each sparrow connection comes with a
cable. - You can make your own custom cable, there's a section later in the readme about custom cables.
- The default cable that sparrow gives you has two RTC Data Channels.
- Each data channel has a
channel.send(data)method, so you can send data. - Each data channel has a
channel.onmessage = event => {}function, so you can receive messages.
- Every message is accounted for. Guaranteed to have no losses (unless a disconnect occurs).
- When a message goes missing — massive catastrophic lag spike, everything halts until the missing message is successfully retransmitted.
- Ideal for important game events like "this player picked up this inventory item" or something like that.
- Not every message will arrive. There will be losses.
- When a message goes missing — it's ignored, and everything continues like nothing happened.
- Ideal for "continuous data" like "now this player is located here" 30 times per second.
- Even when they have your invite code, a user must knock before they can connect.
- The default, is to allow everybody who knocks, like this:
// hosting a sparrow session const sparrow = await Sparrow.host({ // allow everybody allow: async() => true, })
- Each user has a
reputationwhich is a salted hash of their IP address -- making it easy for you to setup a ban list:// make your own ban list const myBanList = new Set() .add("a332f6646c65f738") .add("0d506addf169c407") // hosting a sparrow session const sparrow = await Sparrow.host({ // allow people who are not banned allow: async({reputation}) => { return !myBanList.has(reputation) }, })
- Joiners can also ban hosts!
// joining a sparrow session const sparrow = await Sparrow.join({ // i want to join this invite i found invite: "215fe776f758bc44", // but not if it's this creep! allow: async({reputation}) => reputation !== "a332f6646c65f738", })
- You can also use the allow function as a global switch, to just close the door and prevent any more joiners:
let doorIsOpen = true const sparrow = await Sparrow.host({ allow: async() => doorIsOpen, }) // Close the door! doorIsOpen = false
- And it's async, in case you want to consult your own remote service or something.
connection— you get this object whenever somebody connects to youconst sparrow = await Sparrow.host({ welcome: prospect => connection => { // ephemeral id connection.id // persistent id (based on ip address) connection.reputation // RTCPeerConnection (webrtc internals) connection.peer // the cable you need connection.cable // destroy this connection connection.disconnect() // react to the other side disconnecting return () => {} }, })
- Note, you can also pass a
welcomefunction like this toSparrow.join, but, you don't have to..
- Note, you can also pass a
Sparrow.reportConnectivityhelps you gather statistics about the connectionconst report = await Sparrow.reportConnectivity(connection.peer) report.kind // "local" -- connected directly over local area network // "direct" -- connected directly over the internet // "relay" -- connected via indirect proxy TURN server report.bytesSent // number of bytes sent report.bytesReceived // number of bytes received
prospect— you actually get this object whenever somebody begins attempting a connection to youconst sparrow = await Sparrow.host({ welcome: prospect => { console.log(`${prospect.id} is attempting to connect..`) // ephemeral id prospect.id // persistent id (based on ip address) prospect.reputation // RTCPeerConnection (webrtc internals) prospect.peer // react to connection failure prospect.onFailed(() => { console.log(`${prospect.id} failed to connect`) }) // react to successful connection return connection => { console.log(`${prospect.id} successfully connected`) // react to disconnected return () => {} } }, })
- This isn't necessary, you can just ignore prospects and only concern yourself with connections, if you like.
- Prospects exists as your opportunity to display the activity of attempted connections as they're in progress.
Sparrow.hostandSparrow.joinboth accept these common optionsimport {Sparrow} from "sparrow-rtc" const sparrow = await Sparrow.host({ ...myOtherOptions, // sparrow's official signaller instance (default shown) url: "wss://signaller.sparrow.benev.gg/", // choose which stun and/or turn servers to use (default shown) rtcConfigurator: async() => ({ iceServers: [ // these are free publicly available STUN servers {urls: ["stun:stun.l.google.com:19302"]}, {urls: ["stun:stun.services.mozilla.com:3478"]}, {urls: ["stun:server2024.stunprotocol.org:3478"]}, // if you pay for a TURN service, // you'll obtain some config data that goes in this same array // as these STUN servers above. ], }), })
- We host a free official Sparrow signaller for everybody:
https://signaller.sparrow.benev.gg/- https://signaller.sparrow.benev.gg/health — shows a timestamp if the signaller is up and running
- The Sparrow Signaller does two main things:
- It helps users find each other via invite links.
- It negotiates a complex exchange of WebRTC information, to establish connections between users.
- You can self-host your own instance.
- To allow users to connect to each other, they need to know each other's IP addresses.
- WebRTC usees STUN servers to discover the user IP addresses.
- STUN servers are efficient and cheap to operate, and so there are many publicly available free STUN servers you can use.
- By default, Sparrow will use stun servers by
google.com,mozilla.com, andstunprotocol.org.
- Sometimes users are under network conditions that makes direct connections impossible.
- This happens because the internet is badly designed.
- To save the day, you can configure a TURN server which will act as a reliable relay for those who are unable to achieve direct connections.
- On the up side, with a TURN service, your users can basically always connect reliably.
- On the down side, this costs you money, because you have to pay for all the relayed traffic bandwidth.
- Sparrow does not offer TURN service for free, you'd need to configure your own.
- You can run your own TURN server, like coturn, or you can use a paid cloud service provider like Cloudflare's.
- For some reason these are all a total pain to setup properly.
- To use a TURN server, you just need to include the URL for it in your rtcConfigurator's
iceServers. - Sparrow's signaller does have a little cloudflare integration for my own personal convenience, which you can also take advantage of, but you'd have to self-host the signaller and follow the cloudflare instructions there, to wire it up to your own cloudflare account -- however, you might instead consider spinning up your own coturn TURN server and continue to use the sparrow signaller for free -- either strategy would be a perfectly valid approach.
- For this we recommend the handy tool
concurrentfrom@e280/stz - You can prepare any kinds of RTC Data Channels you like, by establishing your own
cableConfig// import various helpers import {concurrent} from "@e280/stz" import {Sparrow, DataChanneler, concurrent} from "sparrow-rtc" // define your own cable properties (default shown, available as StdCable) export type MyCable = { reliable: RTCDataChannel unreliable: RTCDataChannel } // define your own cable config (default shown) const myCableConfig = Sparrow.asCableConfig<MyCable>({ offering: async peer => { return concurrent({ reliable: DataChanneler.offering(peer, "reliable", { ordered: true, }), unreliable: DataChanneler.offering(peer, "unreliable", { ordered: false, maxRetransmits: 0, }), }) }, answering: async peer => { return concurrent({ reliable: DataChanneler.answering(peer, "reliable"), unreliable: DataChanneler.answering(peer, "unreliable"), }) }, })
- You can then use your cable config for
Sparrow.hostandSparrow.join// your custom cable type // | const sparrow = await Sparrow.host<MyCable>({ ...myOtherOptions, // your custom cable config cableConfig: myCableConfig, })
- Note that Sparrow creates its own special utility rtc data channel called the
conduit, which is reserved for sparrow internal functionality (it sends "bye" notifications when you callconnection.disconnect()to immediately notify the other side)
- You can specify to only log errors like this
const sparrow = await Sparrow.join({ invite: "8ab469956da27aff3825a3681b4f6452", disconnected: () => console.log(`disconnected from host`), // only log errors logging: Sparrow.errorLogging, })
- Of course you can set this
loggingoption onSparrow.hostas well - Three available settings are:
Sparrow.stdLogging-- log everything (default)Sparrow.errorLogging-- only log errorsSparrow.noLogging-- log nothing at all
- Gimme a star on github!
- Join our Benevolent Discord Community, shout at
Chaseand say sparrow-rtc is rad!
