Debug-Modus (?debug): Room-Code + localStorage-Signaling statt QR; STUN entfernt; Polling statt StorageEvent
This commit is contained in:
parent
ab14ac8cc3
commit
2a86156d95
4 changed files with 275 additions and 7 deletions
54
app.js
54
app.js
|
|
@ -8,6 +8,10 @@
|
||||||
|
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
|
|
||||||
|
if (location.search.includes('debug')) {
|
||||||
|
document.body.classList.add('debug-mode');
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Screen routing ─── */
|
/* ─── Screen routing ─── */
|
||||||
function show(name) {
|
function show(name) {
|
||||||
['start','host','join','game','result'].forEach(s =>
|
['start','host','join','game','result'].forEach(s =>
|
||||||
|
|
@ -179,7 +183,15 @@
|
||||||
mesh.onpeerdisconnect = () => {
|
mesh.onpeerdisconnect = () => {
|
||||||
renderPlayers('host-player-list');
|
renderPlayers('host-player-list');
|
||||||
};
|
};
|
||||||
mesh.onqrupdate = d => showQR('qr-host', d);
|
mesh.onqrupdate = d => {
|
||||||
|
showQR('qr-host', d);
|
||||||
|
const raw = $('host-qr-raw');
|
||||||
|
if (raw) raw.textContent = d;
|
||||||
|
const codeEl = $('host-room-code');
|
||||||
|
if (codeEl && mesh.roomCode) codeEl.textContent = mesh.roomCode;
|
||||||
|
const fullSDP = mesh.getFullSDPData ? mesh.getFullSDPData() : null;
|
||||||
|
if (fullSDP) showQR('qr-host-full', fullSDP);
|
||||||
|
};
|
||||||
mesh.onreadyupdate = (ready, total) => {
|
mesh.onreadyupdate = (ready, total) => {
|
||||||
$('btn-start-game').disabled = ready < total || total === 0;
|
$('btn-start-game').disabled = ready < total || total === 0;
|
||||||
};
|
};
|
||||||
|
|
@ -212,6 +224,19 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('btn-host-answer-paste').addEventListener('click', () => {
|
||||||
|
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';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mesh.feedAnswer(raw);
|
||||||
|
$('host-answer-paste').value = '';
|
||||||
|
$('host-scan-status').textContent = 'Verbunden!';
|
||||||
|
});
|
||||||
|
|
||||||
/* ─── Join screen ─── */
|
/* ─── Join screen ─── */
|
||||||
$('btn-join').addEventListener('click', async () => {
|
$('btn-join').addEventListener('click', async () => {
|
||||||
role = 'peer';
|
role = 'peer';
|
||||||
|
|
@ -243,16 +268,41 @@
|
||||||
let scanned = false;
|
let scanned = false;
|
||||||
camScan('join-camera', 'join-camera-canvas', (parsed, raw) => {
|
camScan('join-camera', 'join-camera-canvas', (parsed, raw) => {
|
||||||
if (scanned) return;
|
if (scanned) return;
|
||||||
if (!parsed.s || !parsed.id) return;
|
if (!parsed.id) return;
|
||||||
scanned = true;
|
scanned = true;
|
||||||
$('join-scan-status').textContent = 'Verbinde...';
|
$('join-scan-status').textContent = 'Verbinde...';
|
||||||
|
if (parsed.room) {
|
||||||
|
$('join-room-input').value = parsed.room;
|
||||||
|
mesh.joinFromRoomCode(parsed.room);
|
||||||
|
} else if (parsed.s) {
|
||||||
mesh.joinFromQR(raw);
|
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', () => {
|
||||||
|
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);
|
||||||
|
$('join-qr-paste').value = '';
|
||||||
|
$('join-scan-status').textContent = 'Verbinde...';
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btn-join-room').addEventListener('click', () => {
|
||||||
|
const code = $('join-room-input').value.trim().toUpperCase();
|
||||||
|
if (!code) return;
|
||||||
|
if (!mesh) { show('start'); return; }
|
||||||
|
camStop();
|
||||||
|
$('join-scan-status').textContent = 'Verbinde...';
|
||||||
|
$('join-camera').classList.remove('active');
|
||||||
|
mesh.joinFromRoomCode(code);
|
||||||
|
});
|
||||||
|
|
||||||
/* ─── Host: start game button (in host screen) ─── */
|
/* ─── Host: start game button (in host screen) ─── */
|
||||||
$('btn-start-game').addEventListener('click', () => {
|
$('btn-start-game').addEventListener('click', () => {
|
||||||
setupGame(false);
|
setupGame(false);
|
||||||
|
|
|
||||||
27
index.html
27
index.html
|
|
@ -28,6 +28,11 @@
|
||||||
<div id="qr-host"></div>
|
<div id="qr-host"></div>
|
||||||
<p class="hint">Scannen lassen, um beizutreten</p>
|
<p class="hint">Scannen lassen, um beizutreten</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="room-code debug-only">
|
||||||
|
<span class="room-code-label">Raum-Code</span>
|
||||||
|
<span class="room-code-value" id="host-room-code"></span>
|
||||||
|
<p class="hint">Auf gleichem Rechner: Code in anderem Tab eingeben</p>
|
||||||
|
</div>
|
||||||
<div id="host-players" class="player-section">
|
<div id="host-players" class="player-section">
|
||||||
<h3>Spieler</h3>
|
<h3>Spieler</h3>
|
||||||
<ul id="host-player-list" class="player-list"></ul>
|
<ul id="host-player-list" class="player-list"></ul>
|
||||||
|
|
@ -37,6 +42,19 @@
|
||||||
<video id="host-camera" autoplay playsinline muted></video>
|
<video id="host-camera" autoplay playsinline muted></video>
|
||||||
<canvas id="host-camera-canvas" hidden></canvas>
|
<canvas id="host-camera-canvas" hidden></canvas>
|
||||||
<p id="host-scan-status"></p>
|
<p id="host-scan-status"></p>
|
||||||
|
<details class="debug-helper debug-only">
|
||||||
|
<summary>QR-Rohdaten</summary>
|
||||||
|
<pre id="host-qr-raw" class="raw-data"></pre>
|
||||||
|
</details>
|
||||||
|
<details class="debug-helper debug-only">
|
||||||
|
<summary>Antwort manuell einfügen</summary>
|
||||||
|
<textarea id="host-answer-paste" rows="3" placeholder="Antwort-JSON hier einfügen..."></textarea>
|
||||||
|
<button id="btn-host-answer-paste" class="btn-small">Antwort einspielen</button>
|
||||||
|
</details>
|
||||||
|
<details class="debug-helper debug-only">
|
||||||
|
<summary>Full-SDP-QR (für andere Geräte)</summary>
|
||||||
|
<div id="qr-host-full"></div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-start-game" class="btn-primary" disabled>Münzwurf starten</button>
|
<button id="btn-start-game" class="btn-primary" disabled>Münzwurf starten</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -50,6 +68,15 @@
|
||||||
<video id="join-camera" autoplay playsinline muted></video>
|
<video id="join-camera" autoplay playsinline muted></video>
|
||||||
<canvas id="join-camera-canvas" hidden></canvas>
|
<canvas id="join-camera-canvas" hidden></canvas>
|
||||||
<p id="join-scan-status">QR-Code scannen...</p>
|
<p id="join-scan-status">QR-Code scannen...</p>
|
||||||
|
<div class="debug-only" style="margin-top:12px">
|
||||||
|
<input id="join-room-input" type="text" placeholder="Raum-Code" maxlength="4" style="width:100px;padding:10px;text-align:center;font-size:1.2rem;font-family:monospace;text-transform:uppercase;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text)">
|
||||||
|
<button id="btn-join-room" class="btn-small" style="margin-top:8px">Beitreten</button>
|
||||||
|
</div>
|
||||||
|
<details class="debug-helper debug-only">
|
||||||
|
<summary>QR manuell einfügen</summary>
|
||||||
|
<textarea id="join-qr-paste" rows="3" placeholder="QR-JSON hier einfügen..."></textarea>
|
||||||
|
<button id="btn-join-qr-paste" class="btn-small">Beitreten</button>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div id="join-answer-section" hidden>
|
<div id="join-answer-section" hidden>
|
||||||
<p>Antwort-QR für den Host:</p>
|
<p>Antwort-QR für den Host:</p>
|
||||||
|
|
|
||||||
118
p2p.js
118
p2p.js
|
|
@ -5,11 +5,15 @@ class MeshNet {
|
||||||
this._pendingMeshPeers = new Map();
|
this._pendingMeshPeers = new Map();
|
||||||
this._pendingPeer = null;
|
this._pendingPeer = null;
|
||||||
this._currentQRData = null;
|
this._currentQRData = null;
|
||||||
|
this._fullSDPData = null;
|
||||||
this.isHost = false;
|
this.isHost = false;
|
||||||
this.hostPeerId = null;
|
this.hostPeerId = null;
|
||||||
this.roster = [];
|
this.roster = [];
|
||||||
this.readyPeers = new Set();
|
this.readyPeers = new Set();
|
||||||
this._meshReadySent = false;
|
this._meshReadySent = false;
|
||||||
|
this.roomCode = null;
|
||||||
|
this._processedAnswers = new Set();
|
||||||
|
this._answerPollInterval = null;
|
||||||
|
|
||||||
this.onpeerconnect = null;
|
this.onpeerconnect = null;
|
||||||
this.onpeerdisconnect = null;
|
this.onpeerdisconnect = null;
|
||||||
|
|
@ -19,11 +23,59 @@ class MeshNet {
|
||||||
this.onreadyupdate = null;
|
this.onreadyupdate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_generateRoomCode() {
|
||||||
|
return Math.random().toString(36).substring(2, 6).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
_startAnswerPolling() {
|
||||||
|
this._stopAnswerPolling();
|
||||||
|
this._answerPollInterval = setInterval(() => {
|
||||||
|
if (!this.isHost || !this.roomCode || !this._pendingPeer || this._pendingPeer.signalReceived) return;
|
||||||
|
const prefix = `rp2p_answer_${this.roomCode}_`;
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (key && key.startsWith(prefix) && !this._processedAnswers.has(key)) {
|
||||||
|
this._processedAnswers.add(key);
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
if (!raw) continue;
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
this.feedAnswer(JSON.stringify({ v: 1, id: data.id, s: data.s }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('poll answer error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopAnswerPolling() {
|
||||||
|
if (this._answerPollInterval) {
|
||||||
|
clearInterval(this._answerPollInterval);
|
||||||
|
this._answerPollInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Host: room ─── */
|
/* ─── Host: room ─── */
|
||||||
|
|
||||||
createRoom() {
|
createRoom() {
|
||||||
this.isHost = true;
|
this.isHost = true;
|
||||||
|
this.roomCode = this._generateRoomCode();
|
||||||
|
this._processedAnswers = new Set();
|
||||||
|
this._cleanupStorage();
|
||||||
this._createPendingPeer();
|
this._createPendingPeer();
|
||||||
|
this._startAnswerPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
_cleanupStorage() {
|
||||||
|
const toRemove = [];
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (key && (key.startsWith('rp2p_offer_') || key.startsWith('rp2p_answer_'))) {
|
||||||
|
toRemove.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toRemove.forEach(k => localStorage.removeItem(k));
|
||||||
}
|
}
|
||||||
|
|
||||||
_createPendingPeer() {
|
_createPendingPeer() {
|
||||||
|
|
@ -31,13 +83,23 @@ class MeshNet {
|
||||||
try { this._pendingPeer.peer.destroy(); } catch (e) {}
|
try { this._pendingPeer.peer.destroy(); } catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const peer = new SimplePeer({ initiator: true, trickle: false });
|
const peer = new SimplePeer({ initiator: true, trickle: false, config: { iceServers: [] } });
|
||||||
let signalSent = false;
|
let signalSent = false;
|
||||||
|
|
||||||
peer.on('signal', signal => {
|
peer.on('signal', signal => {
|
||||||
if (signalSent) return;
|
if (signalSent) return;
|
||||||
signalSent = true;
|
signalSent = true;
|
||||||
this._currentQRData = JSON.stringify({ v: 1, id: this.myPeerId, s: signal });
|
|
||||||
|
const sdpJson = JSON.stringify({ v: 1, id: this.myPeerId, s: signal });
|
||||||
|
this._fullSDPData = sdpJson;
|
||||||
|
|
||||||
|
if (this.roomCode) {
|
||||||
|
localStorage.setItem(`rp2p_offer_${this.roomCode}`, sdpJson);
|
||||||
|
this._currentQRData = JSON.stringify({ v: 1, id: this.myPeerId, room: this.roomCode });
|
||||||
|
} else {
|
||||||
|
this._currentQRData = sdpJson;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.onqrupdate) this.onqrupdate(this._currentQRData);
|
if (this.onqrupdate) this.onqrupdate(this._currentQRData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -80,7 +142,7 @@ class MeshNet {
|
||||||
this.hostPeerId = remoteId;
|
this.hostPeerId = remoteId;
|
||||||
const offer = data.s;
|
const offer = data.s;
|
||||||
|
|
||||||
const peer = new SimplePeer({ initiator: false, trickle: false });
|
const peer = new SimplePeer({ initiator: false, trickle: false, config: { iceServers: [] } });
|
||||||
let answerSent = false;
|
let answerSent = false;
|
||||||
|
|
||||||
peer.on('signal', signal => {
|
peer.on('signal', signal => {
|
||||||
|
|
@ -113,10 +175,53 @@ class MeshNet {
|
||||||
this._pendingPeer = { peer, peerId: remoteId, connected: answerSent };
|
this._pendingPeer = { peer, peerId: remoteId, connected: answerSent };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
joinFromRoomCode(code) {
|
||||||
|
const key = `rp2p_offer_${code.toUpperCase()}`;
|
||||||
|
const offerStr = localStorage.getItem(key);
|
||||||
|
if (!offerStr) {
|
||||||
|
console.error('Room not found:', code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const offerData = JSON.parse(offerStr);
|
||||||
|
const remoteId = offerData.id;
|
||||||
|
this.hostPeerId = remoteId;
|
||||||
|
const offer = offerData.s;
|
||||||
|
|
||||||
|
const peer = new SimplePeer({ initiator: false, trickle: false, config: { iceServers: [] } });
|
||||||
|
let answerSent = false;
|
||||||
|
|
||||||
|
peer.on('signal', signal => {
|
||||||
|
if (answerSent) return;
|
||||||
|
answerSent = true;
|
||||||
|
const answerKey = `rp2p_answer_${code.toUpperCase()}_${this.myPeerId}`;
|
||||||
|
localStorage.setItem(answerKey, JSON.stringify({ id: this.myPeerId, s: signal }));
|
||||||
|
});
|
||||||
|
|
||||||
|
peer.on('connect', () => {
|
||||||
|
this.connections.set(remoteId, peer);
|
||||||
|
peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId }));
|
||||||
|
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 ─── */
|
/* ─── Mesh helpers ─── */
|
||||||
|
|
||||||
_createSimplePeer(initiator, onSignal, onConnect) {
|
_createSimplePeer(initiator, onSignal, onConnect) {
|
||||||
const peer = new SimplePeer({ initiator, trickle: false });
|
const peer = new SimplePeer({ initiator, trickle: false, config: { iceServers: [] } });
|
||||||
|
|
||||||
peer.on('signal', signal => {
|
peer.on('signal', signal => {
|
||||||
if (onSignal) onSignal(signal);
|
if (onSignal) onSignal(signal);
|
||||||
|
|
@ -339,7 +444,12 @@ class MeshNet {
|
||||||
return this.connections.size;
|
return this.connections.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFullSDPData() {
|
||||||
|
return this._fullSDPData;
|
||||||
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this._stopAnswerPolling();
|
||||||
for (const peer of this.connections.values()) {
|
for (const peer of this.connections.values()) {
|
||||||
try { peer.destroy(); } catch (e) {}
|
try { peer.destroy(); } catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
81
style.css
81
style.css
|
|
@ -326,5 +326,86 @@ h3 {
|
||||||
.coin.large .coin-face { font-size: 3rem; }
|
.coin.large .coin-face { font-size: 3rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Debug helpers (visible only in ?debug mode) */
|
||||||
|
.debug-only { display: none; }
|
||||||
|
.debug-mode .debug-only { display: revert; }
|
||||||
|
|
||||||
|
.debug-helper {
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.debug-helper summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
.debug-helper summary:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.raw-data {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: var(--text-dim);
|
||||||
|
max-height: 100px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.debug-helper textarea {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
.btn-small {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.btn-small:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Room code (debug mode) */
|
||||||
|
.room-code {
|
||||||
|
margin: 16px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.room-code-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.room-code-value {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 6px;
|
||||||
|
color: var(--gold);
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 2px dashed var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 8px 24px;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
/* Utility */
|
/* Utility */
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue