// ─── Elemente ─── const serverUrlInput = document.getElementById('serverUrl'); const myIdInput = document.getElementById('myId'); const remoteIdInput = document.getElementById('remoteId'); const btnRegister = document.getElementById('btnRegister'); const btnShare = document.getElementById('btnShare'); const btnControl = document.getElementById('btnControl'); const btnStop = document.getElementById('btnStop'); const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); const localPlaceholder = document.getElementById('localPlaceholder'); const remotePlaceholder = document.getElementById('remotePlaceholder'); const statusDot = document.getElementById('statusDot'); const statusText = document.getElementById('statusText'); const peerInfo = document.getElementById('peerInfo'); const remotePanel = document.getElementById('remotePanel'); const logEl = document.getElementById('log'); // ─── State ─── let ws = null; let peerConnection = null; let dataChannel = null; let localStream = null; let controlActive = false; let screenWidth = window.screen.width; let screenHeight = window.screen.height; myIdInput.value = 'PC-' + Math.random().toString(36).substr(2, 4).toUpperCase(); const ICE_SERVERS = [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, { urls: 'stun:stun2.l.google.com:19302' }, ]; // ─── Logging ─── function log(msg, type = '') { const div = document.createElement('div'); div.className = type; div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; logEl.appendChild(div); logEl.scrollTop = logEl.scrollHeight; console.log(msg); } // ─── WebSocket Signaling ─── btnRegister.addEventListener('click', () => { if (ws && ws.readyState === WebSocket.OPEN) { ws.close(); return; } const url = serverUrlInput.value.trim(); ws = new WebSocket(url); ws.onopen = () => { ws.send(JSON.stringify({ type: 'register', peerId: myIdInput.value })); statusDot.className = 'status-dot online'; statusText.textContent = 'Verbunden als ' + myIdInput.value; btnRegister.textContent = 'Trennen'; log('Mit Signal-Server verbunden', 'info'); }; ws.onclose = () => { statusDot.className = 'status-dot offline'; statusText.textContent = 'Nicht verbunden'; btnRegister.textContent = 'Verbinden'; log('Signal-Server getrennt'); }; ws.onerror = (err) => { log('WebSocket-Fehler: ' + err.message, 'error'); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); handleSignal(msg); }; }); async function handleSignal(msg) { switch (msg.type) { case 'registered': log('Registriert als: ' + msg.peerId, 'info'); break; case 'offer': log('Offer empfangen von ' + msg.from, 'info'); peerInfo.textContent = 'Verbunden mit: ' + msg.from; await handleOffer(msg); break; case 'answer': log('Answer empfangen von ' + msg.from, 'info'); if (peerConnection) { await peerConnection.setRemoteDescription(new RTCSessionDescription(msg.sdp)); } break; case 'ice-candidate': if (peerConnection && msg.candidate) { await peerConnection.addIceCandidate(new RTCIceCandidate(msg.candidate)); } break; case 'control': if (window.electronAPI) { window.electronAPI.sendControl(msg.data); } break; } } // ─── WebRTC ─── function createPeerConnection() { if (peerConnection) { peerConnection.close(); } peerConnection = new RTCPeerConnection({ iceServers: ICE_SERVERS }); peerConnection.onicecandidate = (event) => { if (event.candidate) { sendSignal({ type: 'ice-candidate', target: remoteIdInput.value, candidate: event.candidate, }); } }; peerConnection.ontrack = (event) => { remoteVideo.srcObject = event.streams[0]; remotePlaceholder.style.display = 'none'; log('Remote-Stream empfangen', 'info'); }; peerConnection.ondatachannel = (event) => { dataChannel = event.channel; setupDataChannel(dataChannel); log('DataChannel empfangen (Fernsteuerung bereit)', 'info'); }; peerConnection.oniceconnectionstatechange = () => { log('ICE Status: ' + peerConnection.iceConnectionState); if (peerConnection.iceConnectionState === 'disconnected' || peerConnection.iceConnectionState === 'failed') { log('Verbindung verloren!', 'error'); } }; return peerConnection; } function setupDataChannel(dc) { dc.onopen = () => log('DataChannel offen – Fernsteuerung aktiv', 'info'); dc.onclose = () => log('DataChannel geschlossen'); dc.onmessage = (event) => { const data = JSON.parse(event.data); if (window.electronAPI) { window.electronAPI.sendControl(data); } }; } function sendSignal(msg) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(msg)); } } // ─── Bildschirm teilen ─── btnShare.addEventListener('click', async () => { if (!ws || ws.readyState !== WebSocket.OPEN) { log('Zuerst mit Signal-Server verbinden!', 'error'); return; } if (!remoteIdInput.value.trim()) { log('Remote-ID eingeben!', 'error'); return; } try { const sources = await window.electronAPI.getSources(); if (sources.length === 0) { log('Keine Bildschirmquellen gefunden', 'error'); return; } localStream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sources[0].id, maxWidth: 1920, maxHeight: 1080, maxFrameRate: 30, }, }, }); localVideo.srcObject = localStream; localPlaceholder.style.display = 'none'; log('Bildschirm wird erfasst: ' + sources[0].name, 'info'); const pc = createPeerConnection(); dataChannel = pc.createDataChannel('control'); setupDataChannel(dataChannel); localStream.getTracks().forEach((track) => { pc.addTrack(track, localStream); }); const offer = await pc.createOffer(); await pc.setLocalDescription(offer); sendSignal({ type: 'offer', target: remoteIdInput.value, sdp: offer, screenWidth, screenHeight, }); log('Offer gesendet an ' + remoteIdInput.value, 'info'); peerInfo.textContent = 'Verbinde mit: ' + remoteIdInput.value; } catch (err) { log('Fehler: ' + err.message, 'error'); } }); async function handleOffer(msg) { const pc = createPeerConnection(); await pc.setRemoteDescription(new RTCSessionDescription(msg.sdp)); if (msg.screenWidth) screenWidth = msg.screenWidth; if (msg.screenHeight) screenHeight = msg.screenHeight; const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); sendSignal({ type: 'answer', target: msg.from, sdp: answer, }); log('Answer gesendet an ' + msg.from, 'info'); } // ─── Fernsteuerung (Maus/Tastatur über Remote-Video senden) ─── btnControl.addEventListener('click', () => { controlActive = !controlActive; btnControl.textContent = controlActive ? 'Steuerung AN' : 'Fernsteuern'; btnControl.className = controlActive ? 'btn-danger' : 'btn-secondary'; log(controlActive ? 'Fernsteuerung AKTIVIERT' : 'Fernsteuerung DEAKTIVIERT', 'info'); }); function sendControlData(data) { if (!controlActive) return; if (dataChannel && dataChannel.readyState === 'open') { dataChannel.send(JSON.stringify(data)); } else { sendSignal({ type: 'control', target: remoteIdInput.value, data, }); } } function getRelativeCoords(e) { const rect = remoteVideo.getBoundingClientRect(); const videoWidth = remoteVideo.videoWidth || 1920; const videoHeight = remoteVideo.videoHeight || 1080; const scaleX = screenWidth / rect.width; const scaleY = screenHeight / rect.height; return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY, }; } remotePanel.addEventListener('mousemove', (e) => { const coords = getRelativeCoords(e); sendControlData({ action: 'mousemove', ...coords }); }); remotePanel.addEventListener('mousedown', (e) => { e.preventDefault(); const btn = ['left', 'middle', 'right'][e.button] || 'left'; const coords = getRelativeCoords(e); sendControlData({ action: 'mousedown', button: btn, ...coords }); }); remotePanel.addEventListener('mouseup', (e) => { e.preventDefault(); const btn = ['left', 'middle', 'right'][e.button] || 'left'; sendControlData({ action: 'mouseup', button: btn }); }); remotePanel.addEventListener('contextmenu', (e) => e.preventDefault()); remotePanel.addEventListener('wheel', (e) => { if (!controlActive) return; e.preventDefault(); sendControlData({ action: 'scroll', deltaX: Math.sign(e.deltaX) * 3, deltaY: Math.sign(e.deltaY) * 3, }); }, { passive: false }); document.addEventListener('keydown', (e) => { if (!controlActive) return; if (e.target.tagName === 'INPUT') return; e.preventDefault(); const modifiers = []; if (e.ctrlKey) modifiers.push('control'); if (e.shiftKey) modifiers.push('shift'); if (e.altKey) modifiers.push('alt'); sendControlData({ action: 'keydown', key: mapKey(e.key), modifiers, }); }); document.addEventListener('keyup', (e) => { if (!controlActive) return; if (e.target.tagName === 'INPUT') return; e.preventDefault(); sendControlData({ action: 'keyup', key: mapKey(e.key), modifiers: [], }); }); function mapKey(key) { const keyMap = { 'Enter': 'enter', 'Backspace': 'backspace', 'Delete': 'delete', 'Tab': 'tab', 'Escape': 'escape', ' ': 'space', 'ArrowUp': 'up', 'ArrowDown': 'down', 'ArrowLeft': 'left', 'ArrowRight': 'right', 'Home': 'home', 'End': 'end', 'PageUp': 'pageup', 'PageDown': 'pagedown', 'Control': 'control', 'Shift': 'shift', 'Alt': 'alt', '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 keyMap[key] || key.toLowerCase(); } // ─── Stopp ─── btnStop.addEventListener('click', () => { if (localStream) { localStream.getTracks().forEach((t) => t.stop()); localStream = null; localVideo.srcObject = null; localPlaceholder.style.display = ''; } if (peerConnection) { peerConnection.close(); peerConnection = null; } if (dataChannel) { dataChannel.close(); dataChannel = null; } remoteVideo.srcObject = null; remotePlaceholder.style.display = ''; controlActive = false; btnControl.textContent = 'Fernsteuern'; btnControl.className = 'btn-secondary'; peerInfo.textContent = ''; log('Alle Streams gestoppt', 'info'); });