randomp2p/p2p.js
Ole a7933315f3
All checks were successful
Pin to IPFS / pin (push) Successful in 9s
QR-Kompression: pako.deflateRaw + SDP-Dictionary statt base45
- base45/CompressionStream entfernt, durch pako.deflateRaw + base64 ersetzt
- SDP-Dictionary aus 10 echten SDP-JSONs (Chrome+Firefox) → 97.5% Kompression
- Korrupte pako.min.js durch CDN-Version ersetzt
- Tote Altlasten entfernt: hexToBuf, _fullSDPData/getFullSDPData, CRYPTO-UPDATE.md
- Debug-Panel: QR-Click kopiert base64, Raw-Data-Click kopiert JSON
- test-pairing.html an neue pack/unpack-API angepasst
2026-06-14 21:51:49 +02:00

407 lines
12 KiB
JavaScript

class MeshNet {
constructor() {
this.myPeerId = generatePeerId();
this.connections = new Map();
this._pendingMeshPeers = new Map();
this._pendingPeer = null;
this._currentQRData = null;
this.isHost = false;
this.hostPeerId = null;
this.roster = [];
this.readyPeers = new Set();
this._meshReadySent = false;
this.playerName = '';
this.names = new Map();
this.names.set(this.myPeerId, '');
this.onpeerconnect = null;
this.onpeerdisconnect = null;
this.ondata = null;
this.onqrupdate = null;
this.onanswerready = null;
this.onreadyupdate = null;
this.onchat = null;
this.onlog = null;
}
_log(msg) { if (this.onlog) this.onlog(msg); }
/* ─── Host: room ─── */
createRoom() {
this.isHost = true;
this._createPendingPeer();
}
_createPendingPeer() {
if (this._pendingPeer) {
try { this._pendingPeer.peer.destroy(); } catch (e) {}
}
const peer = new SimplePeer({ initiator: true, trickle: false, config: { iceServers: [] } });
let signalSent = false;
peer.on('signal', async signal => {
if (signalSent) return;
signalSent = true;
this._log('SDP-Signal (Offer) empfangen');
this._currentQRData = await pack({ v: 1, id: this.myPeerId, s: signal });
if (this.onqrupdate) this.onqrupdate(this._currentQRData);
this._log('Offer QR erstellt');
});
peer.on('connect', () => {
peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId, name: this.playerName }));
this._log('DataChannel offen, sende Identity');
});
peer.on('data', data => {
try {
const msg = JSON.parse(data.toString());
this._handleMessage(msg, peer);
} catch (e) {
console.error('p2p data error:', e);
}
});
peer.on('close', () => this._cleanupPeer(peer));
peer.on('error', err => console.error('p2p error:', err));
this._pendingPeer = { peer, signalSent, connected: false, peerId: null };
}
async feedAnswer(answerStr) {
if (!this._pendingPeer || this._pendingPeer.signalReceived) {
this._log('feedAnswer ignoriert (bereits verbunden)');
return;
}
try {
const data = await unpack(answerStr);
this._pendingPeer.peerId = data.id;
this._pendingPeer.peer.signal(data.s);
this._pendingPeer.signalReceived = true;
this._log(`Answer verarbeitet von ${data.id.slice(0, 8)}`);
} catch (e) {
console.error('feedAnswer error:', e);
this._log(`feedAnswer Fehler: ${e.message}`);
}
}
/* ─── Peer: join ─── */
async joinFromQR(qrContent) {
const data = await unpack(qrContent);
const remoteId = data.id;
this.hostPeerId = remoteId;
const offer = data.s;
this._log(`QR gelesen, signalisiere Host ${remoteId.slice(0, 8)}`);
const peer = new SimplePeer({ initiator: false, trickle: false, config: { iceServers: [] } });
let answerSent = false;
peer.on('signal', async signal => {
if (answerSent) return;
answerSent = true;
const answerData = await pack({ v: 1, id: this.myPeerId, s: signal, to: remoteId });
if (this.onanswerready) this.onanswerready(answerData);
this._log('Answer QR erstellt');
});
peer.on('connect', () => {
this.connections.set(remoteId, peer);
peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId, name: this.playerName }));
this._log(`DataChannel zu Host ${remoteId.slice(0, 8)} offen`);
if (this.onpeerconnect) this.onpeerconnect(remoteId);
});
peer.on('data', data => {
try {
const msg = JSON.parse(data.toString());
this._handleMessage(msg, peer);
} catch (e) { console.error('p2p data error:', e); }
});
peer.on('close', () => this._cleanupPeer(peer));
peer.on('error', err => {
console.error('p2p error:', err);
this._log(`Peer-Fehler: ${err.message}`);
});
peer.signal(offer);
this.connections.set(remoteId, peer);
this._pendingPeer = { peer, peerId: remoteId, connected: answerSent };
}
/* ─── Mesh helpers ─── */
_createSimplePeer(initiator, onSignal, onConnect) {
const peer = new SimplePeer({ initiator, trickle: false, config: { iceServers: [] } });
peer.on('signal', signal => {
if (onSignal) onSignal(signal);
});
peer.on('connect', () => {
if (onConnect) onConnect();
this._log('DataChannel (Mesh) offen');
});
peer.on('data', data => {
try {
const msg = JSON.parse(data.toString());
this._handleMessage(msg, peer);
} catch (e) { console.error('mesh data error:', e); }
});
peer.on('close', () => this._cleanupPeer(peer));
peer.on('error', e => {
console.error('mesh error:', e);
this._log(`Mesh-Fehler: ${e.message}`);
});
return peer;
}
_initiateMeshConnection(targetPeerId) {
if (this.connections.has(targetPeerId)) return;
if (this._pendingMeshPeers.has(targetPeerId)) return;
if (this.myPeerId >= targetPeerId) return;
this._log(`Mesh zu ${targetPeerId.slice(0, 8)} initiiert`);
const peer = this._createSimplePeer(true,
signal => {
this.sendTo(this.hostPeerId, { type: 'signal_relay', to: targetPeerId, signal });
},
() => {
this.connections.set(targetPeerId, peer);
this._pendingMeshPeers.delete(targetPeerId);
if (this.onpeerconnect) this.onpeerconnect(targetPeerId);
this._checkMeshReady();
}
);
this._pendingMeshPeers.set(targetPeerId, peer);
}
_handleRelayedSignal(fromPeerId, signal) {
this._log(`Relayed-Signal von ${fromPeerId.slice(0, 8)}`);
let peer = this.connections.get(fromPeerId) || this._pendingMeshPeers.get(fromPeerId);
if (!peer) {
peer = this._createSimplePeer(false,
signal => {
this.sendTo(this.hostPeerId, { type: 'signal_relay', to: fromPeerId, signal });
},
() => {
this.connections.set(fromPeerId, peer);
this._pendingMeshPeers.delete(fromPeerId);
if (this.onpeerconnect) this.onpeerconnect(fromPeerId);
this._checkMeshReady();
}
);
this._pendingMeshPeers.set(fromPeerId, peer);
}
peer.signal(signal);
}
_checkMeshConnections() {
const missing = this.roster.filter(pid => pid !== this.myPeerId && !this.connections.has(pid) && this.myPeerId < pid);
if (missing.length) this._log(`Prüfe Mesh: ${missing.length} fehlende Verbindungen`);
for (const pid of this.roster) {
if (pid === this.myPeerId) continue;
if (this.connections.has(pid)) continue;
if (this.myPeerId < pid) {
this._initiateMeshConnection(pid);
}
}
this._checkMeshReady();
}
_checkMeshReady() {
if (this._meshReadySent) return;
if (!this.hostPeerId) return;
if (!this.connections.has(this.hostPeerId)) return;
const allConnected = this.roster.every(pid =>
pid === this.myPeerId || this.connections.has(pid)
);
if (allConnected && this.roster.length > 0) {
this._meshReadySent = true;
this.sendTo(this.hostPeerId, { type: 'mesh_ready' });
this._log('Mesh ready gesendet');
}
}
_broadcastRoster() {
this.roster = this.getPeerIds();
this.readyPeers.clear();
this.broadcast({ type: 'roster', peers: this.roster.map(id => ({ id, name: this.names.get(id) || '' })) });
this.readyPeers.clear();
this._log(`Roster broadcast an ${this.roster.length} Peers`);
if (this.onreadyupdate) this.onreadyupdate(0, this.roster.length);
}
/* ─── Message routing ─── */
_handleMessage(msg, peer) {
const peerId = this._findPeerId(peer) || 'unknown';
if (msg.type === 'identity') {
if (msg.name) this.names.set(msg.peerId, msg.name);
const existing = this._findPeerId(peer);
if (existing && existing !== msg.peerId) {
this.connections.delete(existing);
}
this.connections.set(msg.peerId, peer);
this._log(`Identity: ${msg.peerId.slice(0, 8)}`);
if (this._pendingPeer && this._pendingPeer.peer === peer) {
this._pendingPeer.peerId = msg.peerId;
this._pendingPeer.connected = true;
this._pendingPeer = null;
if (this.onpeerconnect) this.onpeerconnect(msg.peerId);
this._log(`Host: Peer ${msg.peerId.slice(0, 8)} promoted`);
if (this.isHost) {
setTimeout(() => this._createPendingPeer(), 300);
this._broadcastRoster();
}
} else {
if (this.onpeerconnect) this.onpeerconnect(msg.peerId);
this._log(`Mesh: Peer ${msg.peerId.slice(0, 8)} verbunden`);
}
return;
}
if (msg.type === 'signal_relay') {
this._log(`Signal-Relay weitergeleitet an ${msg.to.slice(0, 8)}`);
const target = this.connections.get(msg.to) || this._pendingMeshPeers.get(msg.to);
if (target) {
target.send(JSON.stringify({
type: 'signal_relayed',
from: peerId,
signal: msg.signal
}));
}
return;
}
if (msg.type === 'signal_relayed') {
this._handleRelayedSignal(msg.from, msg.signal);
return;
}
if (msg.type === 'roster') {
this.roster = (msg.peers || []).map(p => p.id || p);
if (msg.peers) msg.peers.forEach(p => { if (p.name) this.names.set(p.id, p.name); });
this._meshReadySent = false;
this._checkMeshConnections();
this._log(`Roster empfangen: ${this.roster.length} Peers`);
return;
}
if (msg.type === 'mesh_ready' && this.isHost) {
this.readyPeers.add(peerId);
this._log(`Mesh ready von ${peerId.slice(0, 8)}`);
if (this.onreadyupdate) {
this.onreadyupdate(this.readyPeers.size, this.roster.length);
}
return;
}
if (msg.type === 'chat') {
if (this.onchat) this.onchat(peerId, msg.text);
return;
}
if (this.ondata) {
this.ondata(peerId, msg);
}
}
/* ─── Peer management ─── */
_findPeerId(peer) {
for (const [id, p] of this.connections) {
if (p === peer) return id;
}
for (const [id, p] of this._pendingMeshPeers) {
if (p === peer) return id;
}
return null;
}
_cleanupPeer(peer) {
const peerId = this._findPeerId(peer);
if (peerId) {
this.connections.delete(peerId);
this._log(`Peer getrennt: ${peerId.slice(0, 8)}`);
if (this.onpeerdisconnect) this.onpeerdisconnect(peerId);
}
for (const [id, p] of this._pendingMeshPeers) {
if (p === peer) {
this._pendingMeshPeers.delete(id);
break;
}
}
if (this._pendingPeer && this._pendingPeer.peer === peer) {
this._pendingPeer = null;
}
}
/* ─── Send ─── */
broadcast(msg) {
const str = JSON.stringify(msg);
for (const [id, peer] of this.connections) {
try { peer.send(str); } catch (e) { console.error('broadcast to', id, 'failed:', e); }
}
}
sendTo(peerId, msg) {
const peer = this.connections.get(peerId);
if (peer) {
try { peer.send(JSON.stringify(msg)); } catch (e) { console.error('sendTo', peerId, 'failed:', e); }
}
}
relaySignal(from, to, signal) {
const target = this.connections.get(to);
if (target) {
target.send(JSON.stringify({ type: 'signal_relay', from, to, signal }));
}
}
getPeerIds() {
return Array.from(this.connections.keys());
}
getCount() {
return this.connections.size;
}
getPeerName(peerId) {
return this.names.get(peerId) || peerId.slice(0, 8);
}
destroy() {
this._log('MeshNet wird zerstört');
for (const peer of this.connections.values()) {
try { peer.destroy(); } catch (e) {}
}
for (const peer of this._pendingMeshPeers.values()) {
try { peer.destroy(); } catch (e) {}
}
this.connections.clear();
this._pendingMeshPeers.clear();
if (this._pendingPeer) {
try { this._pendingPeer.peer.destroy(); } catch (e) {}
this._pendingPeer = null;
}
}
}