Files
aza/APP/screen-share/renderer.js
2026-03-25 14:14:07 +01:00

387 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ─── 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');
});