Establish a WebRTC connection without requiring a signalling server (like TURN- or dedicated server). Instead, initial signalling is achieved by scanning QR codes in person.
WIP Don't use this yet - there are no tests, no error handling and functionality might change!!!
Intended for local games. Setting up a WebRTC connection requires some way of exchanging description- and candidate-data between clients ("signalling"). Usually this is achieved by paying for a TURN server or hosting a dedicated server yourself. However, since everyone is available in-person anyway, the signalling might as well be done via some local exchange - in this case by scanning QR codes.
- Host creates invite as QR code
- Joinee scans invite, which creates accept as QR code
- Host scans accept
- Connection will be established
Check out the example. It requires two devices with a camera to scan the QR codes (smartphones recommended). After the connection is established you can chat via a RTCDataChannel
.
Download using NPM:
npm install webrtc-via-qr
Embed using esm.run:
import { HostConnection, UserConnection } from "https://esm.run/webrtc-via-qr"
The module exposes two classes:
class HostConnection extends RTCPeerConnection
class UserConnection extends RTCPeerConnection
Both are subclasses of RTCPeerConnection, which is at the core of WebRTC. Their only purpose is to simplify the connection process. The actual RTC functionality is inherited - whether you want to add tracks or just require a simple RTCDataChannel.
The config
parameter of the constructor of these subclasses includes one additional optional value: gatheringTimeout
. It defines the upper time limit for the (now hidden) candidate gathering phase in ms and defaults to 1000. Only touch this if the QR code generation fails with no candidates.
Now onto the actual connection process, which is asymmetrical. The peer trying to connect uses a HostConnection
, the responding peer uses a UserConnection
.
const h = new HostConnection(config)
// Step 4
h.addEventListener("connectionstatechange", event => {
if (h.connectionState === "connected") console.log("connected")
})
// Step 1
const inviteCode = await h.createInvite()
// Step 3
h.confirmAccept(acceptCode)
let u = new UserConnection(config)
// Step 4
u.addEventListener("connectionstatechange", event => {
if (u.connectionState === "connected") console.log("connected")
})
// Step 2
const acceptCode = await u.acceptInvite(inviteCode)
Both of these codes inviteCode
& acceptCode
are Strings! The encoding to & decoding from a QR matrix is not included because this should be your choice. QR codes can be customized in so many different ways - error levels, colors, embedded logos, rounded pixels, or even video! Including a certain en-/decoder would be considered bloat by some, while not including enough functionality for others, which is why they are excluded from the scope of this package. The example uses qrcode & jsQR.
- Symmetric NAT. This scenario will still require a TURN server, because there is just no way of exchanging ever-changing combinations of IP + port. This will most likely apply to peers connected by cellular network or if just one peer is behind a local router.
- Topology. Connecting everyone with everyone is infeasable when each connection is initialized by hand. A star topology with a dedicated host-player solves this (but requires additional logic not implemented here).
- Capacity. QR codes can be too small to contain all data. Either some candidates are omitted or multiple codes must be scanned.
- Tight timeout. Some browsers fail the connection on purpose after a set amount of time, making manual exchange unnecessary difficult. Firefox only allows 5 secondy by default (customizable at
media.peerconnection.ice.trickle_grace_period
)
Several other repos already use this mechanism:
However they only provide proof of concept and not a simple package anyone can use.