Files

387 lines
11 KiB
JavaScript
Raw Permalink Normal View History

2026-03-25 14:14:07 +01:00
// ─── 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');
});