Compare commits
3 commits
cef5d35be7
...
87d2d0f190
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87d2d0f190 | ||
|
|
c322e9f31b | ||
|
|
16e9359d1c |
6 changed files with 10159 additions and 10 deletions
45
app.js
45
app.js
|
|
@ -12,6 +12,27 @@
|
|||
document.body.classList.add('debug-mode');
|
||||
}
|
||||
|
||||
document.querySelectorAll('.raw-data').forEach(el => {
|
||||
el.style.cursor = 'pointer';
|
||||
el.title = 'Klicken zum Kopieren';
|
||||
el.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(el.textContent);
|
||||
const orig = el.textContent;
|
||||
el.textContent = 'Kopiert!';
|
||||
setTimeout(() => { el.textContent = orig; }, 1200);
|
||||
} catch (e) {
|
||||
const sel = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
document.execCommand('copy');
|
||||
sel.removeAllRanges();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ─── Screen routing ─── */
|
||||
function show(name) {
|
||||
['start','host','join','game','result'].forEach(s =>
|
||||
|
|
@ -43,7 +64,8 @@
|
|||
const c = $(containerId);
|
||||
if (!c) return;
|
||||
c.innerHTML = '';
|
||||
qrHost = new QRCode(c, { text: data, width: 200, height: 200, correctLevel: QRCode.CorrectLevel.L });
|
||||
const size = Math.min(window.innerWidth - 64, 360);
|
||||
qrHost = new QRCode(c, { text: data, width: size, height: size, correctLevel: QRCode.CorrectLevel.L });
|
||||
}
|
||||
|
||||
/* ─── Camera ─── */
|
||||
|
|
@ -52,14 +74,22 @@
|
|||
|
||||
async function camStart(videoId) {
|
||||
await camStop();
|
||||
try {
|
||||
camStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment', width: { ideal: 320 }, height: { ideal: 240 } }
|
||||
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } }
|
||||
});
|
||||
} catch (_) {
|
||||
camStream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
}
|
||||
const v = $(videoId);
|
||||
v.srcObject = camStream;
|
||||
v.setAttribute('playsinline', '');
|
||||
v.classList.add('active');
|
||||
await v.play();
|
||||
try {
|
||||
const track = camStream.getVideoTracks()[0];
|
||||
if (track) track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] });
|
||||
} catch (_) {}
|
||||
return v;
|
||||
}
|
||||
|
||||
|
|
@ -71,13 +101,16 @@
|
|||
function camScan(videoId, canvasId, callback) {
|
||||
const v = $(videoId);
|
||||
const c = $(canvasId);
|
||||
const ctx = c.getContext('2d');
|
||||
camRaf = requestAnimationFrame(function tick() {
|
||||
const ctx = c.getContext('2d', { willReadFrequently: true });
|
||||
let lastScan = 0;
|
||||
camRaf = requestAnimationFrame(function tick(now) {
|
||||
if (!v.videoWidth) { camRaf = requestAnimationFrame(tick); return; }
|
||||
if (now - lastScan < 100) { camRaf = requestAnimationFrame(tick); return; }
|
||||
lastScan = now;
|
||||
c.width = v.videoWidth; c.height = v.videoHeight;
|
||||
ctx.drawImage(v, 0, 0);
|
||||
const img = ctx.getImageData(0, 0, c.width, c.height);
|
||||
const code = jsQR(img.data, img.width, img.height);
|
||||
const code = jsQR(img.data, img.width, img.height, { inversionAttempts: 'dontInvert' });
|
||||
if (code && code.data) {
|
||||
try {
|
||||
const p = JSON.parse(code.data);
|
||||
|
|
@ -244,6 +277,8 @@
|
|||
mesh.onanswerready = answerData => {
|
||||
$('join-answer-section').hidden = false;
|
||||
showQR('qr-answer', answerData);
|
||||
const raw = $('answer-raw');
|
||||
if (raw) raw.textContent = answerData;
|
||||
$('join-scan-status').textContent = 'Antwort-QR dem Host zeigen';
|
||||
camStop();
|
||||
};
|
||||
|
|
|
|||
10
index.html
10
index.html
|
|
@ -69,6 +69,10 @@
|
|||
<p>Antwort-QR für den Host:</p>
|
||||
<div id="qr-answer"></div>
|
||||
<p class="hint">Zeige diesen QR-Code dem Host</p>
|
||||
<details class="debug-helper debug-only" style="margin-top:8px">
|
||||
<summary>QR-Rohdaten</summary>
|
||||
<pre id="answer-raw" class="raw-data"></pre>
|
||||
</details>
|
||||
</div>
|
||||
<ul id="join-player-list" class="player-list"></ul>
|
||||
</div>
|
||||
|
|
@ -121,9 +125,9 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<script src="https://unpkg.com/simple-peer@9.11.1/simplepeer.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
|
||||
<script src="vendor/simplepeer.min.js"></script>
|
||||
<script src="vendor/qrcode.min.js"></script>
|
||||
<script src="vendor/jsQR.js"></script>
|
||||
<script src="crypto.js"></script>
|
||||
<script src="p2p.js"></script>
|
||||
<script src="protocol.js"></script>
|
||||
|
|
|
|||
1
p2p.js
1
p2p.js
|
|
@ -233,6 +233,7 @@ class MeshNet {
|
|||
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);
|
||||
|
|
|
|||
10102
vendor/jsQR.js
vendored
Normal file
10102
vendor/jsQR.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
vendor/qrcode.min.js
vendored
Normal file
1
vendor/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
vendor/simplepeer.min.js
vendored
Normal file
6
vendor/simplepeer.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue