661 lines
22 KiB
JavaScript
661 lines
22 KiB
JavaScript
|
|
const { ipcRenderer } = require('electron');
|
|||
|
|
const Peer = require('peerjs').default || require('peerjs');
|
|||
|
|
|
|||
|
|
let input = null;
|
|||
|
|
try { input = require('./input'); } catch {}
|
|||
|
|
|
|||
|
|
// ─── DOM ───
|
|||
|
|
const myIdEl = document.getElementById('my-id');
|
|||
|
|
const remoteIdInput = document.getElementById('remote-id');
|
|||
|
|
const remoteKeyInput = document.getElementById('remote-key');
|
|||
|
|
const btnConnect = document.getElementById('btn-connect');
|
|||
|
|
const btnShare = document.getElementById('btn-share');
|
|||
|
|
const btnCopy = document.getElementById('btn-copy');
|
|||
|
|
const btnKill = document.getElementById('btn-kill');
|
|||
|
|
const btnRenew = document.getElementById('btn-renew');
|
|||
|
|
const myKeyEl = document.getElementById('my-key');
|
|||
|
|
const killOverlay = document.getElementById('kill-overlay');
|
|||
|
|
const killClose = document.getElementById('kill-close');
|
|||
|
|
const rcIndicator = document.getElementById('rc-indicator');
|
|||
|
|
const cbAot = document.getElementById('cb-aot');
|
|||
|
|
const cbCtrl = document.getElementById('cb-ctrl');
|
|||
|
|
const remoteVideo = document.getElementById('remote-video');
|
|||
|
|
const statusEl = document.getElementById('status-text');
|
|||
|
|
const statusDot = document.getElementById('dot');
|
|||
|
|
const placeholder = document.getElementById('placeholder');
|
|||
|
|
const toast = document.getElementById('toast');
|
|||
|
|
const trustBadge = document.getElementById('trust-badge');
|
|||
|
|
const savedPeersEl = document.getElementById('saved-peers');
|
|||
|
|
|
|||
|
|
// ─── Persistente Zugangsdaten ───
|
|||
|
|
function generateId() { return String(Math.floor(100000 + Math.random() * 900000)); }
|
|||
|
|
function generateKey() {
|
|||
|
|
const c = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|||
|
|
let k = '';
|
|||
|
|
for (let i = 0; i < 8; i++) k += c[Math.floor(Math.random() * c.length)];
|
|||
|
|
return k;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let MY_ID = localStorage.getItem('savedPeerId');
|
|||
|
|
let SECRET_KEY = localStorage.getItem('savedSecretKey');
|
|||
|
|
if (!MY_ID || !SECRET_KEY) {
|
|||
|
|
MY_ID = generateId();
|
|||
|
|
SECRET_KEY = generateKey();
|
|||
|
|
localStorage.setItem('savedPeerId', MY_ID);
|
|||
|
|
localStorage.setItem('savedSecretKey', SECRET_KEY);
|
|||
|
|
}
|
|||
|
|
myKeyEl.textContent = SECRET_KEY;
|
|||
|
|
|
|||
|
|
// ─── Gespeicherte Verbindungen ───
|
|||
|
|
let savedConns = JSON.parse(localStorage.getItem('savedConnections') || '{}');
|
|||
|
|
|
|||
|
|
function saveSavedConns() {
|
|||
|
|
localStorage.setItem('savedConnections', JSON.stringify(savedConns));
|
|||
|
|
renderSavedPeers();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── State ───
|
|||
|
|
let localStream = null;
|
|||
|
|
let sharing = false;
|
|||
|
|
let dataConn = null;
|
|||
|
|
let incomingConn = null;
|
|||
|
|
let activeCall = null;
|
|||
|
|
let incomingCall = null;
|
|||
|
|
let remoteW = null;
|
|||
|
|
let remoteH = null;
|
|||
|
|
let windowFocused = true;
|
|||
|
|
let killed = false;
|
|||
|
|
let connected = false;
|
|||
|
|
let authenticatedPeers = new Set();
|
|||
|
|
|
|||
|
|
window.addEventListener('focus', () => { windowFocused = true; });
|
|||
|
|
window.addEventListener('blur', () => { windowFocused = false; });
|
|||
|
|
|
|||
|
|
// ─── Auto-fill letzte Verbindung ───
|
|||
|
|
const lastPeerId = localStorage.getItem('lastConnectedPeer') || '';
|
|||
|
|
if (lastPeerId) {
|
|||
|
|
remoteIdInput.value = lastPeerId;
|
|||
|
|
if (savedConns[lastPeerId]) {
|
|||
|
|
remoteKeyInput.value = savedConns[lastPeerId].key;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
updateTrustBadge();
|
|||
|
|
|
|||
|
|
function updateTrustBadge() {
|
|||
|
|
const tid = remoteIdInput.value.trim();
|
|||
|
|
if (savedConns[tid]) {
|
|||
|
|
trustBadge.classList.add('show');
|
|||
|
|
if (!remoteKeyInput.value.trim()) {
|
|||
|
|
remoteKeyInput.value = savedConns[tid].key;
|
|||
|
|
}
|
|||
|
|
remoteKeyInput.classList.add('trusted');
|
|||
|
|
} else {
|
|||
|
|
trustBadge.classList.remove('show');
|
|||
|
|
remoteKeyInput.classList.remove('trusted');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
remoteIdInput.addEventListener('input', () => {
|
|||
|
|
const tid = remoteIdInput.value.trim();
|
|||
|
|
if (savedConns[tid]) {
|
|||
|
|
remoteKeyInput.value = savedConns[tid].key;
|
|||
|
|
} else {
|
|||
|
|
remoteKeyInput.value = '';
|
|||
|
|
}
|
|||
|
|
updateTrustBadge();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── Gespeicherte Peers im Placeholder anzeigen ───
|
|||
|
|
function renderSavedPeers() {
|
|||
|
|
if (!savedPeersEl) return;
|
|||
|
|
const peers = Object.entries(savedConns);
|
|||
|
|
if (peers.length === 0) {
|
|||
|
|
savedPeersEl.classList.remove('show');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
savedPeersEl.innerHTML = '<div class="saved-peers-title">Gespeicherte Verbindungen</div>';
|
|||
|
|
peers.sort((a, b) => (b[1].lastUsed || 0) - (a[1].lastUsed || 0));
|
|||
|
|
peers.forEach(([id, data]) => {
|
|||
|
|
const item = document.createElement('div');
|
|||
|
|
item.className = 'saved-peer-item';
|
|||
|
|
const timeStr = data.lastUsed ? new Date(data.lastUsed).toLocaleDateString('de-DE') : '';
|
|||
|
|
item.innerHTML =
|
|||
|
|
'<span class="saved-peer-id">' + id + '</span>' +
|
|||
|
|
'<span class="saved-peer-time">' + timeStr + '</span>' +
|
|||
|
|
'<button class="saved-peer-remove" title="Entfernen">×</button>';
|
|||
|
|
item.querySelector('.saved-peer-id').addEventListener('click', () => {
|
|||
|
|
remoteIdInput.value = id;
|
|||
|
|
remoteKeyInput.value = data.key;
|
|||
|
|
updateTrustBadge();
|
|||
|
|
});
|
|||
|
|
item.querySelector('.saved-peer-remove').addEventListener('click', (e) => {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
delete savedConns[id];
|
|||
|
|
saveSavedConns();
|
|||
|
|
updateTrustBadge();
|
|||
|
|
});
|
|||
|
|
savedPeersEl.appendChild(item);
|
|||
|
|
});
|
|||
|
|
savedPeersEl.classList.add('show');
|
|||
|
|
}
|
|||
|
|
renderSavedPeers();
|
|||
|
|
|
|||
|
|
// ─── UI Helpers ───
|
|||
|
|
function setStatus(msg, color) {
|
|||
|
|
statusEl.textContent = msg;
|
|||
|
|
if (color) statusEl.style.color = color;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setOnline(on) {
|
|||
|
|
statusDot.classList.toggle('online', on);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showToast(msg, isKill) {
|
|||
|
|
toast.textContent = msg;
|
|||
|
|
toast.className = 'toast show' + (isKill ? ' kill' : '');
|
|||
|
|
setTimeout(() => toast.classList.remove('show'), 2200);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateRcIndicator() {
|
|||
|
|
const active = connected && ((dataConn && dataConn.open) || (incomingConn && incomingConn.open));
|
|||
|
|
rcIndicator.classList.toggle('show', !!active && cbCtrl.checked && !killed);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════
|
|||
|
|
// DISCONNECT: Verbindung trennen, Peer bleibt
|
|||
|
|
// ═══════════════════════════════════════════
|
|||
|
|
function disconnect() {
|
|||
|
|
if (activeCall) { try { activeCall.close(); } catch {} activeCall = null; }
|
|||
|
|
if (incomingCall) { try { incomingCall.close(); } catch {} incomingCall = null; }
|
|||
|
|
if (dataConn) { try { dataConn.close(); } catch {} dataConn = null; }
|
|||
|
|
if (incomingConn) { try { incomingConn.close(); } catch {} incomingConn = null; }
|
|||
|
|
|
|||
|
|
remoteVideo.srcObject = null;
|
|||
|
|
connected = false;
|
|||
|
|
remoteW = null;
|
|||
|
|
remoteH = null;
|
|||
|
|
|
|||
|
|
rcIndicator.classList.remove('show');
|
|||
|
|
placeholder.classList.remove('hidden');
|
|||
|
|
document.body.classList.add('no-stream');
|
|||
|
|
|
|||
|
|
if (!killed) {
|
|||
|
|
setStatus('Bereit – warte auf Verbindung', '#2ea043');
|
|||
|
|
}
|
|||
|
|
showToast('Verbindung getrennt');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════
|
|||
|
|
// KILL: Alles zerstören (Notaus)
|
|||
|
|
// ═══════════════════════════════════════════
|
|||
|
|
function killAll(showOverlay) {
|
|||
|
|
disconnect();
|
|||
|
|
|
|||
|
|
if (localStream) {
|
|||
|
|
localStream.getTracks().forEach((t) => t.stop());
|
|||
|
|
localStream = null;
|
|||
|
|
}
|
|||
|
|
sharing = false;
|
|||
|
|
|
|||
|
|
try { peer.disconnect(); } catch {}
|
|||
|
|
try { peer.destroy(); } catch {}
|
|||
|
|
|
|||
|
|
killed = true;
|
|||
|
|
|
|||
|
|
setOnline(false);
|
|||
|
|
btnShare.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> Screen teilen';
|
|||
|
|
btnShare.classList.remove('active');
|
|||
|
|
|
|||
|
|
setStatus('OFFLINE – Alle Verbindungen gekappt', '#e5534b');
|
|||
|
|
|
|||
|
|
if (showOverlay) {
|
|||
|
|
killOverlay.classList.add('show');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function hardKill() {
|
|||
|
|
killAll(false);
|
|||
|
|
|
|||
|
|
const h2 = killOverlay.querySelector('h2');
|
|||
|
|
const p = killOverlay.querySelector('p');
|
|||
|
|
if (h2) h2.textContent = 'SYSTEM OFFLINE';
|
|||
|
|
if (p) p.textContent = 'ALLE VERBINDUNGEN GEKAPPT';
|
|||
|
|
killOverlay.classList.add('show');
|
|||
|
|
|
|||
|
|
setTimeout(() => ipcRenderer.send('cleanup-done'), 1000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
btnKill.addEventListener('click', () => disconnect());
|
|||
|
|
killClose.addEventListener('click', () => killOverlay.classList.remove('show'));
|
|||
|
|
|
|||
|
|
ipcRenderer.on('kill-switch', () => disconnect());
|
|||
|
|
ipcRenderer.on('hard-kill', () => hardKill());
|
|||
|
|
|
|||
|
|
function guardKilled() {
|
|||
|
|
if (killed) {
|
|||
|
|
setStatus('Session beendet – App neustarten', '#e5534b');
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── PeerJS ───
|
|||
|
|
const peer = new Peer(MY_ID, {
|
|||
|
|
config: {
|
|||
|
|
iceServers: [
|
|||
|
|
{ urls: 'stun:stun.l.google.com:19302' },
|
|||
|
|
{ urls: 'stun:stun1.l.google.com:19302' },
|
|||
|
|
{ urls: 'stun:stun2.l.google.com:19302' },
|
|||
|
|
{ urls: 'stun:stun3.l.google.com:19302' },
|
|||
|
|
{ urls: 'stun:stun4.l.google.com:19302' },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
peer.on('open', (id) => {
|
|||
|
|
if (killed) return;
|
|||
|
|
myIdEl.textContent = id;
|
|||
|
|
setOnline(true);
|
|||
|
|
setStatus('Bereit – warte auf Verbindung', '#2ea043');
|
|||
|
|
ipcRenderer.invoke('save-credentials', MY_ID, SECRET_KEY);
|
|||
|
|
autoShare();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
async function autoShare() {
|
|||
|
|
if (sharing || killed) return;
|
|||
|
|
try {
|
|||
|
|
const sources = await ipcRenderer.invoke('get-sources');
|
|||
|
|
if (!sources.length) return;
|
|||
|
|
localStream = await navigator.mediaDevices.getUserMedia({
|
|||
|
|
audio: false,
|
|||
|
|
video: { mandatory: {
|
|||
|
|
chromeMediaSource: 'desktop',
|
|||
|
|
chromeMediaSourceId: sources[0].id,
|
|||
|
|
maxWidth: 1920, maxHeight: 1080, maxFrameRate: 30,
|
|||
|
|
}},
|
|||
|
|
});
|
|||
|
|
sharing = true;
|
|||
|
|
btnShare.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Stop';
|
|||
|
|
btnShare.classList.add('active');
|
|||
|
|
setStatus('Bereit – warte auf Verbindung', '#2ea043');
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
peer.on('error', (err) => {
|
|||
|
|
if (killed) return;
|
|||
|
|
if (err.type === 'unavailable-id') {
|
|||
|
|
setStatus('ID belegt – neuer Versuch...', '#e5534b');
|
|||
|
|
setTimeout(() => { if (!killed) location.reload(); }, 800);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setOnline(false);
|
|||
|
|
setStatus('Fehler: ' + err.type, '#e5534b');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
peer.on('disconnected', () => {
|
|||
|
|
if (killed) return;
|
|||
|
|
setOnline(false);
|
|||
|
|
setStatus('Server-Verbindung verloren – Reconnect...', '#e5534b');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (!killed && !peer.destroyed) {
|
|||
|
|
try { peer.reconnect(); } catch {}
|
|||
|
|
}
|
|||
|
|
}, 3000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── ID + Key kopieren ───
|
|||
|
|
btnCopy.addEventListener('click', () => {
|
|||
|
|
const id = myIdEl.textContent;
|
|||
|
|
if (id && id !== '------') {
|
|||
|
|
navigator.clipboard.writeText(id + ' / ' + SECRET_KEY);
|
|||
|
|
btnCopy.classList.add('copied');
|
|||
|
|
showToast('ID + Key kopiert!');
|
|||
|
|
setTimeout(() => btnCopy.classList.remove('copied'), 2000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
cbAot.addEventListener('change', () => ipcRenderer.send('set-always-on-top', cbAot.checked));
|
|||
|
|
cbCtrl.addEventListener('change', () => updateRcIndicator());
|
|||
|
|
|
|||
|
|
btnRenew.addEventListener('click', () => {
|
|||
|
|
if (guardKilled()) return;
|
|||
|
|
MY_ID = generateId();
|
|||
|
|
SECRET_KEY = generateKey();
|
|||
|
|
localStorage.setItem('savedPeerId', MY_ID);
|
|||
|
|
localStorage.setItem('savedSecretKey', SECRET_KEY);
|
|||
|
|
showToast('Neue Zugangsdaten generiert – Neustart...');
|
|||
|
|
ipcRenderer.invoke('save-credentials', MY_ID, SECRET_KEY).then(() => {
|
|||
|
|
setTimeout(() => location.reload(), 1200);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════
|
|||
|
|
// EINGEHENDE VERBINDUNG: Auth prüfen
|
|||
|
|
// ═══════════════════════════════════════════
|
|||
|
|
peer.on('connection', (conn) => {
|
|||
|
|
if (killed) { conn.close(); return; }
|
|||
|
|
|
|||
|
|
conn.on('open', () => {
|
|||
|
|
if (killed) { conn.close(); return; }
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
conn.on('data', (d) => {
|
|||
|
|
if (d.t === 'auth') {
|
|||
|
|
if (d.key === SECRET_KEY) {
|
|||
|
|
authenticatedPeers.add(conn.peer);
|
|||
|
|
conn.send({ t: 'auth-ok' });
|
|||
|
|
showToast('Verbindung akzeptiert: ' + conn.peer);
|
|||
|
|
|
|||
|
|
incomingConn = conn;
|
|||
|
|
connected = true;
|
|||
|
|
|
|||
|
|
if (input) {
|
|||
|
|
const s = input.getScreenSize();
|
|||
|
|
conn.send({ t: 'scr', w: s.width, h: s.height });
|
|||
|
|
}
|
|||
|
|
updateRcIndicator();
|
|||
|
|
setStatus('Verbunden mit ' + conn.peer, '#00b4d8');
|
|||
|
|
} else {
|
|||
|
|
conn.send({ t: 'auth-fail' });
|
|||
|
|
showToast('Zugang verweigert: falscher Key', true);
|
|||
|
|
setTimeout(() => conn.close(), 500);
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
handleRemoteData(d);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
conn.on('close', () => {
|
|||
|
|
if (incomingConn === conn) {
|
|||
|
|
incomingConn = null;
|
|||
|
|
connected = false;
|
|||
|
|
updateRcIndicator();
|
|||
|
|
placeholder.classList.remove('hidden');
|
|||
|
|
document.body.classList.add('no-stream');
|
|||
|
|
remoteVideo.srcObject = null;
|
|||
|
|
if (!killed) {
|
|||
|
|
setStatus('Bereit – warte auf Verbindung', '#2ea043');
|
|||
|
|
showToast('Gegenstelle hat getrennt');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── Eingehender Anruf: nur authentifizierte Peers ───
|
|||
|
|
peer.on('call', (call) => {
|
|||
|
|
if (killed) return;
|
|||
|
|
|
|||
|
|
if (!authenticatedPeers.has(call.peer)) {
|
|||
|
|
showToast('Anruf abgelehnt: nicht authentifiziert', true);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
call.answer(localStream || undefined);
|
|||
|
|
incomingCall = call;
|
|||
|
|
|
|||
|
|
call.on('stream', (stream) => {
|
|||
|
|
if (killed) return;
|
|||
|
|
remoteVideo.srcObject = stream;
|
|||
|
|
placeholder.classList.add('hidden');
|
|||
|
|
document.body.classList.remove('no-stream');
|
|||
|
|
setStatus('Verbunden mit ' + call.peer, '#00b4d8');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
call.on('close', () => {
|
|||
|
|
if (incomingCall === call) { incomingCall = null; }
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── Remote Data Handler ───
|
|||
|
|
function handleRemoteData(d) {
|
|||
|
|
if (killed) return;
|
|||
|
|
if (d.t === 'scr') {
|
|||
|
|
remoteW = d.w; remoteH = d.h;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!input) return;
|
|||
|
|
if (!cbCtrl.checked) return;
|
|||
|
|
try {
|
|||
|
|
switch (d.t) {
|
|||
|
|
case 'mm': input.moveMouse(d.x|0, d.y|0); break;
|
|||
|
|
case 'md': input.moveMouse(d.x|0, d.y|0); input.mouseToggle('down', d.b||'left'); break;
|
|||
|
|
case 'mu': input.mouseToggle('up', d.b||'left'); break;
|
|||
|
|
case 'mc': input.moveMouse(d.x|0, d.y|0); input.mouseClick(d.b||'left'); break;
|
|||
|
|
case 'sc': input.scrollMouse(d.dx||0, d.dy||0); break;
|
|||
|
|
case 'kd': input.keyToggle(d.k, 'down'); break;
|
|||
|
|
case 'ku': input.keyToggle(d.k, 'up'); break;
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Screen Share Toggle ───
|
|||
|
|
btnShare.addEventListener('click', async () => {
|
|||
|
|
if (guardKilled()) return;
|
|||
|
|
if (sharing) {
|
|||
|
|
if (localStream) localStream.getTracks().forEach((t) => t.stop());
|
|||
|
|
localStream = null;
|
|||
|
|
sharing = false;
|
|||
|
|
btnShare.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> Screen teilen';
|
|||
|
|
btnShare.classList.remove('active');
|
|||
|
|
setStatus('Stream gestoppt', '#8b949e');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
const sources = await ipcRenderer.invoke('get-sources');
|
|||
|
|
if (!sources.length) { setStatus('Keine Quelle!', '#e5534b'); return; }
|
|||
|
|
localStream = await navigator.mediaDevices.getUserMedia({
|
|||
|
|
audio: false,
|
|||
|
|
video: { mandatory: {
|
|||
|
|
chromeMediaSource: 'desktop',
|
|||
|
|
chromeMediaSourceId: sources[0].id,
|
|||
|
|
maxWidth: 1920, maxHeight: 1080, maxFrameRate: 30,
|
|||
|
|
}},
|
|||
|
|
});
|
|||
|
|
sharing = true;
|
|||
|
|
btnShare.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Stop';
|
|||
|
|
btnShare.classList.add('active');
|
|||
|
|
setStatus('Capture aktiv', '#2ea043');
|
|||
|
|
} catch (e) {
|
|||
|
|
setStatus('Fehler: ' + e.message, '#e5534b');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════
|
|||
|
|
// VERBINDEN: Auth-Handshake + Screen-Call
|
|||
|
|
// ═══════════════════════════════════════════
|
|||
|
|
btnConnect.addEventListener('click', async () => {
|
|||
|
|
if (guardKilled()) return;
|
|||
|
|
const tid = remoteIdInput.value.trim();
|
|||
|
|
let tkey = remoteKeyInput.value.trim();
|
|||
|
|
|
|||
|
|
if (!tid) { setStatus('Remote-ID eingeben!', '#e5534b'); return; }
|
|||
|
|
|
|||
|
|
if (!tkey && savedConns[tid]) {
|
|||
|
|
tkey = savedConns[tid].key;
|
|||
|
|
remoteKeyInput.value = tkey;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!tkey) {
|
|||
|
|
setStatus('Remote-Key eingeben!', '#e5534b');
|
|||
|
|
remoteKeyInput.focus();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!sharing || !localStream) await autoShare();
|
|||
|
|
if (!localStream) { setStatus('Screen-Capture fehlgeschlagen!', '#e5534b'); return; }
|
|||
|
|
|
|||
|
|
setStatus('Verbinde...', '#e5534b');
|
|||
|
|
btnConnect.disabled = true;
|
|||
|
|
|
|||
|
|
dataConn = peer.connect(tid, { reliable: true });
|
|||
|
|
|
|||
|
|
const authTimeout = setTimeout(() => {
|
|||
|
|
if (!connected) {
|
|||
|
|
setStatus('Zeitüberschreitung – Gegenstelle nicht erreichbar', '#e5534b');
|
|||
|
|
btnConnect.disabled = false;
|
|||
|
|
if (dataConn) { try { dataConn.close(); } catch {} dataConn = null; }
|
|||
|
|
}
|
|||
|
|
}, 15000);
|
|||
|
|
|
|||
|
|
dataConn.on('open', () => {
|
|||
|
|
if (killed) return;
|
|||
|
|
setStatus('Authentifiziere...', '#e5534b');
|
|||
|
|
dataConn.send({ t: 'auth', key: tkey });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
dataConn.on('data', (d) => {
|
|||
|
|
if (d.t === 'auth-ok') {
|
|||
|
|
clearTimeout(authTimeout);
|
|||
|
|
connected = true;
|
|||
|
|
btnConnect.disabled = false;
|
|||
|
|
|
|||
|
|
savedConns[tid] = { key: tkey, lastUsed: Date.now() };
|
|||
|
|
saveSavedConns();
|
|||
|
|
localStorage.setItem('lastConnectedPeer', tid);
|
|||
|
|
updateTrustBadge();
|
|||
|
|
|
|||
|
|
activeCall = peer.call(tid, localStream);
|
|||
|
|
activeCall.on('stream', (s) => {
|
|||
|
|
if (killed) return;
|
|||
|
|
remoteVideo.srcObject = s;
|
|||
|
|
placeholder.classList.add('hidden');
|
|||
|
|
document.body.classList.remove('no-stream');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (input) {
|
|||
|
|
const s = input.getScreenSize();
|
|||
|
|
dataConn.send({ t: 'scr', w: s.width, h: s.height });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setStatus('Verbunden: ' + tid, '#00b4d8');
|
|||
|
|
showToast('Verbunden mit ' + tid);
|
|||
|
|
updateRcIndicator();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (d.t === 'auth-fail') {
|
|||
|
|
clearTimeout(authTimeout);
|
|||
|
|
btnConnect.disabled = false;
|
|||
|
|
setStatus('Falscher Key! Zugang verweigert.', '#e5534b');
|
|||
|
|
showToast('Falscher Key!', true);
|
|||
|
|
|
|||
|
|
if (savedConns[tid]) {
|
|||
|
|
delete savedConns[tid];
|
|||
|
|
saveSavedConns();
|
|||
|
|
updateTrustBadge();
|
|||
|
|
remoteKeyInput.value = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (dataConn) { try { dataConn.close(); } catch {} dataConn = null; }
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleRemoteData(d);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
dataConn.on('close', () => {
|
|||
|
|
clearTimeout(authTimeout);
|
|||
|
|
if (connected) {
|
|||
|
|
connected = false;
|
|||
|
|
dataConn = null;
|
|||
|
|
updateRcIndicator();
|
|||
|
|
showToast('Verbindung getrennt');
|
|||
|
|
if (!killed) setStatus('Bereit – warte auf Verbindung', '#2ea043');
|
|||
|
|
placeholder.classList.remove('hidden');
|
|||
|
|
document.body.classList.add('no-stream');
|
|||
|
|
remoteVideo.srcObject = null;
|
|||
|
|
} else {
|
|||
|
|
btnConnect.disabled = false;
|
|||
|
|
dataConn = null;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
dataConn.on('error', (err) => {
|
|||
|
|
clearTimeout(authTimeout);
|
|||
|
|
btnConnect.disabled = false;
|
|||
|
|
setStatus('Verbindungsfehler: ' + (err.type || err.message || err), '#e5534b');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── Koordinaten skalieren ───
|
|||
|
|
function scale(e) {
|
|||
|
|
const r = remoteVideo.getBoundingClientRect();
|
|||
|
|
return {
|
|||
|
|
x: ((e.clientX - r.left) / r.width) * (remoteW || remoteVideo.videoWidth || 1920),
|
|||
|
|
y: ((e.clientY - r.top) / r.height) * (remoteH || remoteVideo.videoHeight || 1080),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function sendCtrl(obj) {
|
|||
|
|
if (killed || !connected) return;
|
|||
|
|
if (!cbCtrl.checked) return;
|
|||
|
|
const conn = dataConn || incomingConn;
|
|||
|
|
if (!conn || !conn.open) return;
|
|||
|
|
conn.send(obj);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Maus-Events ───
|
|||
|
|
remoteVideo.addEventListener('mousemove', (e) => {
|
|||
|
|
const c = scale(e); sendCtrl({ t:'mm', x:c.x, y:c.y });
|
|||
|
|
});
|
|||
|
|
remoteVideo.addEventListener('mousedown', (e) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
const c = scale(e);
|
|||
|
|
sendCtrl({ t:'md', x:c.x, y:c.y, b:['left','middle','right'][e.button]||'left' });
|
|||
|
|
});
|
|||
|
|
remoteVideo.addEventListener('mouseup', (e) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
sendCtrl({ t:'mu', b:['left','middle','right'][e.button]||'left' });
|
|||
|
|
});
|
|||
|
|
remoteVideo.addEventListener('wheel', (e) => {
|
|||
|
|
if (!cbCtrl.checked || killed || !connected) return;
|
|||
|
|
e.preventDefault();
|
|||
|
|
sendCtrl({ t:'sc', dx:Math.sign(e.deltaX)*3, dy:Math.sign(e.deltaY)*3 });
|
|||
|
|
}, { passive: false });
|
|||
|
|
remoteVideo.addEventListener('contextmenu', (e) => e.preventDefault());
|
|||
|
|
|
|||
|
|
// ─── Tastatur ───
|
|||
|
|
document.addEventListener('keydown', (e) => {
|
|||
|
|
if (e.target.tagName === 'INPUT') return;
|
|||
|
|
if (e.key === 'Escape') { disconnect(); return; }
|
|||
|
|
if (killed || !cbCtrl.checked || !connected) return;
|
|||
|
|
e.preventDefault();
|
|||
|
|
sendCtrl({ t:'kd', k:mapKey(e.key) });
|
|||
|
|
});
|
|||
|
|
document.addEventListener('keyup', (e) => {
|
|||
|
|
if (e.target.tagName === 'INPUT') return;
|
|||
|
|
if (e.key === 'Escape') return;
|
|||
|
|
if (killed || !cbCtrl.checked || !connected) return;
|
|||
|
|
e.preventDefault();
|
|||
|
|
sendCtrl({ t:'ku', k:mapKey(e.key) });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function mapKey(k) {
|
|||
|
|
const m = {
|
|||
|
|
Enter:'enter',Backspace:'backspace',Delete:'delete',Tab:'tab',
|
|||
|
|
' ':'space',
|
|||
|
|
ArrowUp:'up',ArrowDown:'down',ArrowLeft:'left',ArrowRight:'right',
|
|||
|
|
Home:'home',End:'end',PageUp:'pageup',PageDown:'pagedown',
|
|||
|
|
Control:'control',Shift:'shift',Alt:'alt',Insert:'insert',
|
|||
|
|
F1:'f1',F2:'f2',F3:'f3',F4:'f4',F5:'f5',F6:'f6',
|
|||
|
|
F7:'f7',F8:'f8',F9:'f9',F10:'f10',F11:'f11',F12:'f12',
|
|||
|
|
};
|
|||
|
|
return m[k] || k.toLowerCase();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Auto-Dim ───
|
|||
|
|
let dimTimer;
|
|||
|
|
document.addEventListener('mousemove', () => {
|
|||
|
|
const bar = document.getElementById('bar');
|
|||
|
|
bar.classList.remove('dim');
|
|||
|
|
clearTimeout(dimTimer);
|
|||
|
|
if (!document.body.classList.contains('no-stream')) {
|
|||
|
|
dimTimer = setTimeout(() => bar.classList.add('dim'), 3000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── Fenster-Close Guard ───
|
|||
|
|
window.addEventListener('beforeunload', () => {
|
|||
|
|
if (!killed) killAll(false);
|
|||
|
|
});
|