QR-Kompression: deflate-raw + base45 für kleinere, scanbare SDP-QRs
All checks were successful
Pin to IPFS / pin (push) Successful in 10s
All checks were successful
Pin to IPFS / pin (push) Successful in 10s
This commit is contained in:
parent
3d2cb93847
commit
3ff0c15dfd
3 changed files with 76 additions and 23 deletions
31
app.js
31
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...';
|
||||
});
|
||||
|
|
|
|||
48
crypto.js
48
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);
|
||||
}
|
||||
|
|
|
|||
20
p2p.js
20
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);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue