Pair Two Devices with QR
Problem
A desktop app (host) and a phone (guest) need to establish a Mesh P2P session with no backend and no typing: the host displays its invitation as a QR code, the phone scans it, and the phone shows its answer as a QR code the host's webcam scans back.
Solution
meshQrCodec (async) compresses pairing JSON with deflate-raw + base64url and prefixes it mq1. — comfortably inside QR byte capacity. Sigil renders the code on each side; ore-qr-scanner (or createQrScanner) reads the counterparty's screen.
ts
import { createMeshGuest, createMeshHost, meshQrCodec } from '@vielzeug/mesh';
import { createQrScanner, encodeQr, toSvg } from '@vielzeug/sigil';
// ── Host (desktop): show the invitation, scan the answer ──────────────
const host = createMeshHost();
const invitation = await host.createInvitation();
// meshQrCodec.encode is async — await before rendering.
const invitationText = await meshQrCodec.encode(invitation); // "mq1.eNpN…"
document.querySelector('#qr-out')!.innerHTML = toSvg(encodeQr(invitationText), {
label: 'Pairing invitation',
});
const webcam = document.querySelector('video')!;
const scanner = createQrScanner({ video: webcam });
scanner.onResult(async ({ value }) => {
// decode accepts mq1.* and plain meshCodec text — paste works too.
const answer = await meshQrCodec.decode(value);
const peer = await host.acceptAnswer(answer);
console.log('paired with', peer.id);
});
await scanner.start();
// ── Guest (phone): scan the invitation, show the answer ───────────────
const guest = createMeshGuest();
const phoneCam = document.querySelector('video')!;
const guestScanner = createQrScanner({ video: phoneCam });
guestScanner.onResult(async ({ value }) => {
// value is the host's "mq1.*" invitation text
const answer = await guest.acceptInvitation(await meshQrCodec.decode(value));
const answerText = await meshQrCodec.encode(answer);
document.querySelector('#qr-answer')!.innerHTML = toSvg(encodeQr(answerText), {
label: 'Pairing answer',
});
});
await guestScanner.start();With the Refine components
html
<!-- host page -->
<ore-qr-code id="invite" variant="card"></ore-qr-code>
<ore-qr-scanner id="cam"></ore-qr-scanner>
<script type="module">
const invite = document.getElementById('invite');
const cam = document.getElementById('cam');
invite.setAttribute('value', await meshQrCodec.encode(await host.createInvitation()));
cam.addEventListener('scan', async (e) => {
const answer = await meshQrCodec.decode(e.detail.value); // scan event carries the raw string
await host.acceptAnswer(answer);
});
cam.setAttribute('active', '');
</script>Pitfalls
meshQrCodec.encode/decodeare async (streams-based compression) — awaiting them inside render code is a common source of[object Promise]payloads.CompressionStream/DecompressionStreamexist in modern browsers and Node 18+, but not everywhere —encodethrowsMeshUnsupportedErrorwhere they are missing. Keep the plainmeshCodecpaste flow as fallback;decodealready accepts both formats.- Camera scanning requires a secure context (
httpsorlocalhost) and permission — always wire theerrorevent so a denial surfaces instead of a dead UI. - The answer QR changes per attempt (fresh session id) — re-render on every new
createInvitation/acceptInvitationcall, don't cache the image. - Scanned strings are untrusted:
meshQrCodec.decodevalidates shape and throwsMeshPairingErroron corrupt input — catch it before callingacceptAnswer.