randomp2p/p2p.js
Ole 09c36ac6c1
All checks were successful
Pin to IPFS / pin (push) Successful in 10s
SDP-Minify: TCP/IPv6/Non-Host-Candidates vor pack() entfernt
2026-06-14 17:22:31 +02:00

382 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class MeshNet {
constructor() {
this.myPeerId = generatePeerId();
this.connections = new Map();
this._pendingMeshPeers = new Map();
this._pendingPeer = null;
this._currentQRData = null;
this._fullSDPData = 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;
}
/* ─── 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;
const minSignal = signal.sdp ? { ...signal, sdp: stripSDP(signal.sdp) } : signal;
const packed = await pack({ v: 1, id: this.myPeerId, s: minSignal });
this._currentQRData = encode45(packed);
this._fullSDPData = this._currentQRData;
if (this.onqrupdate) this.onqrupdate(this._currentQRData);
});
peer.on('connect', () => {
peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId, name: this.playerName }));
});
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) return;
try {
const data = await unpack(decode45(answerStr));
this._pendingPeer.peerId = data.id;
this._pendingPeer.peer.signal(data.s);
this._pendingPeer.signalReceived = true;
} catch (e) {
console.error('feedAnswer error:', e);
}
}
/* ─── Peer: join ─── */
async joinFromQR(qrContent) {
const data = await unpack(decode45(qrContent));
const remoteId = data.id;
this.hostPeerId = remoteId;
const offer = data.s;
const peer = new SimplePeer({ initiator: false, trickle: false, config: { iceServers: [] } });
let answerSent = false;
peer.on('signal', async signal => {
if (answerSent) return;
answerSent = true;
const minSignal = signal.sdp ? { ...signal, sdp: stripSDP(signal.sdp) } : signal;
const packed = await pack({ v: 1, id: this.myPeerId, s: minSignal, to: remoteId });
const answerData = encode45(packed);
if (this.onanswerready) this.onanswerready(answerData);
});
peer.on('connect', () => {
this.connections.set(remoteId, peer);
peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId, name: this.playerName }));
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));
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();
// No identity exchange peerId known from context
});
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));
return peer;
}
_initiateMeshConnection(targetPeerId) {
if (this.connections.has(targetPeerId)) return;
if (this._pendingMeshPeers.has(targetPeerId)) return;
if (this.myPeerId >= targetPeerId) return;
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) {
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() {
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' });
}
}
_broadcastRoster() {
this.roster = this.getPeerIds();
this.readyPeers.clear();
this.broadcast({ type: 'roster', peers: this.roster.map(id => ({ id, name: this.names.get(id) || '' })) });
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);
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);
if (this.isHost) {
setTimeout(() => this._createPendingPeer(), 300);
this._broadcastRoster();
}
} else {
if (this.onpeerconnect) this.onpeerconnect(msg.peerId);
}
return;
}
if (msg.type === 'signal_relay') {
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();
return;
}
if (msg.type === 'mesh_ready' && this.isHost) {
this.readyPeers.add(peerId);
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);
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);
}
getFullSDPData() {
return this._fullSDPData;
}
destroy() {
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;
}
}
}