diff --git a/app.js b/app.js index c26093c..0d71de8 100644 --- a/app.js +++ b/app.js @@ -104,7 +104,7 @@ const c = $(canvasId); const ctx = c.getContext('2d', { willReadFrequently: true }); let lastScan = 0; - camRaf = requestAnimationFrame(function tick(now) { + camRaf = requestAnimationFrame(async function tick(now) { if (!v.videoWidth) { camRaf = requestAnimationFrame(tick); return; } if (now - lastScan < 100) { camRaf = requestAnimationFrame(tick); return; } lastScan = now; @@ -114,7 +114,8 @@ const code = jsQR(img.data, img.width, img.height, { inversionAttempts: 'dontInvert' }); if (code && code.data) { try { - const p = JSON.parse(code.data); + const bytes = decode45(code.data); + const p = await unpack(bytes); if (p.v === 1) { callback(p, code.data); return; } } catch(e) {} } @@ -278,13 +279,13 @@ await camStart('host-camera'); $('host-scan-status').textContent = 'Warte auf Antwort-QR...'; scanningHost = true; - camScan('host-camera', 'host-camera-canvas', (parsed, raw) => { + camScan('host-camera', 'host-camera-canvas', async (parsed, raw) => { if (!parsed.s || !parsed.id) return; if (parsed.to && parsed.to !== mesh.myPeerId) return; camStop(); scanningHost = false; $('host-scan-status').textContent = 'Verbunden!'; - mesh.feedAnswer(raw); + await mesh.feedAnswer(raw); $('btn-scan-answer').textContent = 'Antwort-QR scannen'; }); $('btn-scan-answer').textContent = 'Scannen beenden'; @@ -293,15 +294,17 @@ } }); - $('btn-host-answer-paste').addEventListener('click', () => { + $('btn-host-answer-paste').addEventListener('click', async () => { const raw = $('host-answer-paste').value.trim(); if (!raw) return; if (!mesh) return; - try { JSON.parse(raw); } catch (e) { - $('host-scan-status').textContent = 'Ungültiges JSON'; + try { + await unpack(decode45(raw)); + } catch (e) { + $('host-scan-status').textContent = 'Ungültige Daten'; return; } - mesh.feedAnswer(raw); + await mesh.feedAnswer(raw); $('host-answer-paste').value = ''; $('host-scan-status').textContent = 'Verbunden!'; }); @@ -340,24 +343,26 @@ try { await camStart('join-camera'); let scanned = false; - camScan('join-camera', 'join-camera-canvas', (parsed, raw) => { + camScan('join-camera', 'join-camera-canvas', async (parsed, raw) => { if (scanned) return; if (!parsed.s || !parsed.id) return; scanned = true; $('join-scan-status').textContent = 'Verbinde...'; - mesh.joinFromQR(raw); + await mesh.joinFromQR(raw); }); } catch (e) { $('join-scan-status').textContent = 'Kamera nicht verfügbar'; } }); - $('btn-join-qr-paste').addEventListener('click', () => { + $('btn-join-qr-paste').addEventListener('click', async () => { const raw = $('join-qr-paste').value.trim(); if (!raw) return; if (!mesh) { show('start'); return; } - try { JSON.parse(raw); } catch (e) { return; } - mesh.joinFromQR(raw); + try { + await unpack(decode45(raw)); + } catch (e) { return; } + await mesh.joinFromQR(raw); $('join-qr-paste').value = ''; $('join-scan-status').textContent = 'Verbinde...'; }); diff --git a/crypto.js b/crypto.js index 8e110fb..e77955b 100644 --- a/crypto.js +++ b/crypto.js @@ -68,3 +68,51 @@ function hexToBuf(hex) { } return bytes; } + +/* ─── QR compression: deflate-raw + base45 ─── */ + +async function pack(obj) { + const json = JSON.stringify(obj); + const blob = new Blob([json]); + const cs = new CompressionStream('deflate-raw'); + const ab = await new Response(blob.stream().pipeThrough(cs)).arrayBuffer(); + return new Uint8Array(ab); +} + +async function unpack(bytes) { + const blob = new Blob([bytes]); + const ds = new DecompressionStream('deflate-raw'); + const ab = await new Response(blob.stream().pipeThrough(ds)).arrayBuffer(); + return JSON.parse(new TextDecoder().decode(ab)); +} + +const B45 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; + +function encode45(buf) { + const bytes = new Uint8Array(buf); + let s = ''; + for (let i = 0; i < bytes.length; i += 2) { + if (i + 1 < bytes.length) { + const v = bytes[i] * 256 + bytes[i + 1]; + s += B45[v % 45] + B45[Math.floor(v / 45) % 45] + B45[Math.floor(v / 2025)]; + } else { + const v = bytes[i]; + s += B45[v % 45] + B45[Math.floor(v / 45)]; + } + } + return s; +} + +function decode45(str) { + const bytes = []; + for (let i = 0; i < str.length; i += 3) { + if (i + 2 < str.length) { + const v = B45.indexOf(str[i]) + B45.indexOf(str[i + 1]) * 45 + B45.indexOf(str[i + 2]) * 2025; + bytes.push(Math.floor(v / 256), v % 256); + } else { + const v = B45.indexOf(str[i]) + B45.indexOf(str[i + 1]) * 45; + bytes.push(v); + } + } + return Uint8Array.from(bytes); +} diff --git a/p2p.js b/p2p.js index 3fd04db..43c21c2 100644 --- a/p2p.js +++ b/p2p.js @@ -38,11 +38,12 @@ class MeshNet { const peer = new SimplePeer({ initiator: true, trickle: false, config: { iceServers: [] } }); let signalSent = false; - peer.on('signal', signal => { + peer.on('signal', async signal => { if (signalSent) return; signalSent = true; - this._currentQRData = JSON.stringify({ v: 1, id: this.myPeerId, s: signal }); + const packed = await pack({ v: 1, id: this.myPeerId, s: signal }); + this._currentQRData = encode45(packed); this._fullSDPData = this._currentQRData; if (this.onqrupdate) this.onqrupdate(this._currentQRData); @@ -67,10 +68,10 @@ class MeshNet { this._pendingPeer = { peer, signalSent, connected: false, peerId: null }; } - feedAnswer(answerStr) { + async feedAnswer(answerStr) { if (!this._pendingPeer || this._pendingPeer.signalReceived) return; try { - const data = JSON.parse(answerStr); + const data = await unpack(decode45(answerStr)); this._pendingPeer.peerId = data.id; this._pendingPeer.peer.signal(data.s); this._pendingPeer.signalReceived = true; @@ -81,8 +82,8 @@ class MeshNet { /* ─── Peer: join ─── */ - joinFromQR(qrContent) { - const data = JSON.parse(qrContent); + async joinFromQR(qrContent) { + const data = await unpack(decode45(qrContent)); const remoteId = data.id; this.hostPeerId = remoteId; const offer = data.s; @@ -90,12 +91,11 @@ class MeshNet { const peer = new SimplePeer({ initiator: false, trickle: false, config: { iceServers: [] } }); let answerSent = false; - peer.on('signal', signal => { + peer.on('signal', async signal => { if (answerSent) return; answerSent = true; - const answerData = JSON.stringify({ - v: 1, id: this.myPeerId, s: signal, to: remoteId - }); + const packed = await pack({ v: 1, id: this.myPeerId, s: signal, to: remoteId }); + const answerData = encode45(packed); if (this.onanswerready) this.onanswerready(answerData); });