QR-Kompression: deflate-raw + base45 für kleinere, scanbare SDP-QRs
All checks were successful
Pin to IPFS / pin (push) Successful in 10s

This commit is contained in:
Ole 2026-06-14 17:05:36 +02:00
parent 3d2cb93847
commit 3ff0c15dfd
3 changed files with 76 additions and 23 deletions

31
app.js
View file

@ -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...';
});

View file

@ -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);
}

20
p2p.js
View file

@ -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);
});