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 c = $(canvasId);
|
||||||
const ctx = c.getContext('2d', { willReadFrequently: true });
|
const ctx = c.getContext('2d', { willReadFrequently: true });
|
||||||
let lastScan = 0;
|
let lastScan = 0;
|
||||||
camRaf = requestAnimationFrame(function tick(now) {
|
camRaf = requestAnimationFrame(async function tick(now) {
|
||||||
if (!v.videoWidth) { camRaf = requestAnimationFrame(tick); return; }
|
if (!v.videoWidth) { camRaf = requestAnimationFrame(tick); return; }
|
||||||
if (now - lastScan < 100) { camRaf = requestAnimationFrame(tick); return; }
|
if (now - lastScan < 100) { camRaf = requestAnimationFrame(tick); return; }
|
||||||
lastScan = now;
|
lastScan = now;
|
||||||
|
|
@ -114,7 +114,8 @@
|
||||||
const code = jsQR(img.data, img.width, img.height, { inversionAttempts: 'dontInvert' });
|
const code = jsQR(img.data, img.width, img.height, { inversionAttempts: 'dontInvert' });
|
||||||
if (code && code.data) {
|
if (code && code.data) {
|
||||||
try {
|
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; }
|
if (p.v === 1) { callback(p, code.data); return; }
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
@ -278,13 +279,13 @@
|
||||||
await camStart('host-camera');
|
await camStart('host-camera');
|
||||||
$('host-scan-status').textContent = 'Warte auf Antwort-QR...';
|
$('host-scan-status').textContent = 'Warte auf Antwort-QR...';
|
||||||
scanningHost = true;
|
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.s || !parsed.id) return;
|
||||||
if (parsed.to && parsed.to !== mesh.myPeerId) return;
|
if (parsed.to && parsed.to !== mesh.myPeerId) return;
|
||||||
camStop();
|
camStop();
|
||||||
scanningHost = false;
|
scanningHost = false;
|
||||||
$('host-scan-status').textContent = 'Verbunden!';
|
$('host-scan-status').textContent = 'Verbunden!';
|
||||||
mesh.feedAnswer(raw);
|
await mesh.feedAnswer(raw);
|
||||||
$('btn-scan-answer').textContent = 'Antwort-QR scannen';
|
$('btn-scan-answer').textContent = 'Antwort-QR scannen';
|
||||||
});
|
});
|
||||||
$('btn-scan-answer').textContent = 'Scannen beenden';
|
$('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();
|
const raw = $('host-answer-paste').value.trim();
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
if (!mesh) return;
|
if (!mesh) return;
|
||||||
try { JSON.parse(raw); } catch (e) {
|
try {
|
||||||
$('host-scan-status').textContent = 'Ungültiges JSON';
|
await unpack(decode45(raw));
|
||||||
|
} catch (e) {
|
||||||
|
$('host-scan-status').textContent = 'Ungültige Daten';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mesh.feedAnswer(raw);
|
await mesh.feedAnswer(raw);
|
||||||
$('host-answer-paste').value = '';
|
$('host-answer-paste').value = '';
|
||||||
$('host-scan-status').textContent = 'Verbunden!';
|
$('host-scan-status').textContent = 'Verbunden!';
|
||||||
});
|
});
|
||||||
|
|
@ -340,24 +343,26 @@
|
||||||
try {
|
try {
|
||||||
await camStart('join-camera');
|
await camStart('join-camera');
|
||||||
let scanned = false;
|
let scanned = false;
|
||||||
camScan('join-camera', 'join-camera-canvas', (parsed, raw) => {
|
camScan('join-camera', 'join-camera-canvas', async (parsed, raw) => {
|
||||||
if (scanned) return;
|
if (scanned) return;
|
||||||
if (!parsed.s || !parsed.id) return;
|
if (!parsed.s || !parsed.id) return;
|
||||||
scanned = true;
|
scanned = true;
|
||||||
$('join-scan-status').textContent = 'Verbinde...';
|
$('join-scan-status').textContent = 'Verbinde...';
|
||||||
mesh.joinFromQR(raw);
|
await mesh.joinFromQR(raw);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$('join-scan-status').textContent = 'Kamera nicht verfügbar';
|
$('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();
|
const raw = $('join-qr-paste').value.trim();
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
if (!mesh) { show('start'); return; }
|
if (!mesh) { show('start'); return; }
|
||||||
try { JSON.parse(raw); } catch (e) { return; }
|
try {
|
||||||
mesh.joinFromQR(raw);
|
await unpack(decode45(raw));
|
||||||
|
} catch (e) { return; }
|
||||||
|
await mesh.joinFromQR(raw);
|
||||||
$('join-qr-paste').value = '';
|
$('join-qr-paste').value = '';
|
||||||
$('join-scan-status').textContent = 'Verbinde...';
|
$('join-scan-status').textContent = 'Verbinde...';
|
||||||
});
|
});
|
||||||
|
|
|
||||||
48
crypto.js
48
crypto.js
|
|
@ -68,3 +68,51 @@ function hexToBuf(hex) {
|
||||||
}
|
}
|
||||||
return bytes;
|
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: [] } });
|
const peer = new SimplePeer({ initiator: true, trickle: false, config: { iceServers: [] } });
|
||||||
let signalSent = false;
|
let signalSent = false;
|
||||||
|
|
||||||
peer.on('signal', signal => {
|
peer.on('signal', async signal => {
|
||||||
if (signalSent) return;
|
if (signalSent) return;
|
||||||
signalSent = true;
|
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;
|
this._fullSDPData = this._currentQRData;
|
||||||
|
|
||||||
if (this.onqrupdate) this.onqrupdate(this._currentQRData);
|
if (this.onqrupdate) this.onqrupdate(this._currentQRData);
|
||||||
|
|
@ -67,10 +68,10 @@ class MeshNet {
|
||||||
this._pendingPeer = { peer, signalSent, connected: false, peerId: null };
|
this._pendingPeer = { peer, signalSent, connected: false, peerId: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
feedAnswer(answerStr) {
|
async feedAnswer(answerStr) {
|
||||||
if (!this._pendingPeer || this._pendingPeer.signalReceived) return;
|
if (!this._pendingPeer || this._pendingPeer.signalReceived) return;
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(answerStr);
|
const data = await unpack(decode45(answerStr));
|
||||||
this._pendingPeer.peerId = data.id;
|
this._pendingPeer.peerId = data.id;
|
||||||
this._pendingPeer.peer.signal(data.s);
|
this._pendingPeer.peer.signal(data.s);
|
||||||
this._pendingPeer.signalReceived = true;
|
this._pendingPeer.signalReceived = true;
|
||||||
|
|
@ -81,8 +82,8 @@ class MeshNet {
|
||||||
|
|
||||||
/* ─── Peer: join ─── */
|
/* ─── Peer: join ─── */
|
||||||
|
|
||||||
joinFromQR(qrContent) {
|
async joinFromQR(qrContent) {
|
||||||
const data = JSON.parse(qrContent);
|
const data = await unpack(decode45(qrContent));
|
||||||
const remoteId = data.id;
|
const remoteId = data.id;
|
||||||
this.hostPeerId = remoteId;
|
this.hostPeerId = remoteId;
|
||||||
const offer = data.s;
|
const offer = data.s;
|
||||||
|
|
@ -90,12 +91,11 @@ class MeshNet {
|
||||||
const peer = new SimplePeer({ initiator: false, trickle: false, config: { iceServers: [] } });
|
const peer = new SimplePeer({ initiator: false, trickle: false, config: { iceServers: [] } });
|
||||||
let answerSent = false;
|
let answerSent = false;
|
||||||
|
|
||||||
peer.on('signal', signal => {
|
peer.on('signal', async signal => {
|
||||||
if (answerSent) return;
|
if (answerSent) return;
|
||||||
answerSent = true;
|
answerSent = true;
|
||||||
const answerData = JSON.stringify({
|
const packed = await pack({ v: 1, id: this.myPeerId, s: signal, to: remoteId });
|
||||||
v: 1, id: this.myPeerId, s: signal, to: remoteId
|
const answerData = encode45(packed);
|
||||||
});
|
|
||||||
if (this.onanswerready) this.onanswerready(answerData);
|
if (this.onanswerready) this.onanswerready(answerData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue