This commit is contained in:
2026-03-25 14:14:07 +01:00
parent d6b31e2ef7
commit a0073b4fb1
10368 changed files with 2214340 additions and 0 deletions

163
APP/screen-share/index.html Normal file
View File

@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Screen Share</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #eee;
display: flex;
flex-direction: column;
height: 100vh;
}
.toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: #16213e;
border-bottom: 1px solid #0f3460;
flex-wrap: wrap;
}
.toolbar label { font-size: 13px; color: #aaa; }
.toolbar input, .toolbar select {
background: #0f3460;
border: 1px solid #533483;
color: #eee;
padding: 6px 10px;
border-radius: 4px;
font-size: 13px;
}
.toolbar input:focus, .toolbar select:focus { outline: none; border-color: #e94560; }
.toolbar button {
padding: 6px 16px;
border: none;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
font-weight: 600;
}
.btn-primary { background: #e94560; color: #fff; }
.btn-primary:hover { background: #c73650; }
.btn-success { background: #27ae60; color: #fff; }
.btn-success:hover { background: #219a52; }
.btn-danger { background: #c0392b; color: #fff; }
.btn-danger:hover { background: #a93226; }
.btn-secondary { background: #533483; color: #fff; }
.btn-secondary:hover { background: #432a6b; }
.status-bar {
padding: 6px 16px;
background: #0f3460;
font-size: 12px;
display: flex;
justify-content: space-between;
}
.status-dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-dot.online { background: #27ae60; }
.status-dot.offline { background: #c0392b; }
.video-container {
flex: 1;
display: flex;
gap: 4px;
padding: 4px;
min-height: 0;
}
.video-panel {
flex: 1;
position: relative;
background: #111;
border-radius: 6px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.video-panel video {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
.video-label {
position: absolute;
top: 8px;
left: 8px;
background: rgba(0,0,0,0.7);
padding: 3px 10px;
border-radius: 3px;
font-size: 12px;
}
.video-placeholder {
color: #555;
font-size: 14px;
text-align: center;
}
#log {
max-height: 120px;
overflow-y: auto;
padding: 8px 16px;
background: #111;
font-family: 'Consolas', monospace;
font-size: 11px;
color: #0f0;
border-top: 1px solid #333;
}
#log div { padding: 1px 0; }
#log .error { color: #e94560; }
#log .info { color: #3498db; }
</style>
</head>
<body>
<div class="toolbar">
<label>Signal-Server:</label>
<input type="text" id="serverUrl" value="ws://localhost:9090" style="width:200px">
<label>Meine ID:</label>
<input type="text" id="myId" style="width:100px">
<button class="btn-primary" id="btnRegister">Verbinden</button>
<span style="width:20px"></span>
<label>Remote-ID:</label>
<input type="text" id="remoteId" style="width:100px">
<button class="btn-success" id="btnShare">Bildschirm teilen</button>
<button class="btn-secondary" id="btnControl">Fernsteuern</button>
<button class="btn-danger" id="btnStop">Stopp</button>
</div>
<div class="status-bar">
<span><span class="status-dot offline" id="statusDot"></span><span id="statusText">Nicht verbunden</span></span>
<span id="peerInfo"></span>
</div>
<div class="video-container">
<div class="video-panel">
<div class="video-label">Mein Bildschirm</div>
<video id="localVideo" autoplay muted></video>
<div class="video-placeholder" id="localPlaceholder">Klicke "Bildschirm teilen"</div>
</div>
<div class="video-panel" id="remotePanel">
<div class="video-label">Remote-Bildschirm</div>
<video id="remoteVideo" autoplay></video>
<div class="video-placeholder" id="remotePlaceholder">Warte auf Verbindung...</div>
</div>
</div>
<div id="log"></div>
<script src="renderer.js"></script>
</body>
</html>

75
APP/screen-share/main.js Normal file
View File

@@ -0,0 +1,75 @@
const { app, BrowserWindow, desktopCapturer, ipcMain } = require('electron');
const path = require('path');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.loadFile('index.html');
}
ipcMain.handle('get-sources', async () => {
const sources = await desktopCapturer.getSources({
types: ['screen', 'window'],
thumbnailSize: { width: 320, height: 180 },
});
return sources.map((s) => ({
id: s.id,
name: s.name,
thumbnail: s.thumbnail.toDataURL(),
}));
});
let robotjs = null;
try {
robotjs = require('robotjs');
} catch (e) {
console.warn('robotjs nicht verfuegbar Fernsteuerung deaktiviert');
}
ipcMain.on('remote-control', (_event, data) => {
if (!robotjs) return;
try {
switch (data.action) {
case 'mousemove':
robotjs.moveMouse(Math.round(data.x), Math.round(data.y));
break;
case 'mousedown':
robotjs.mouseToggle('down', data.button || 'left');
break;
case 'mouseup':
robotjs.mouseToggle('up', data.button || 'left');
break;
case 'click':
robotjs.mouseClick(data.button || 'left');
break;
case 'keydown':
robotjs.keyToggle(data.key, 'down', data.modifiers || []);
break;
case 'keyup':
robotjs.keyToggle(data.key, 'up', data.modifiers || []);
break;
case 'scroll':
robotjs.scrollMouse(data.deltaX || 0, data.deltaY || 0);
break;
}
} catch (err) {
console.error('Fernsteuerungsfehler:', err.message);
}
});
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
app.quit();
});

View File

@@ -0,0 +1,16 @@
{
"name": "screen-share-app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"signal": "node signal-server/server.js"
},
"dependencies": {
"electron": "^33.0.0",
"robotjs": "^0.6.0"
},
"devDependencies": {
"electron-rebuild": "^3.2.9"
}
}

View File

@@ -0,0 +1,6 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
getSources: () => ipcRenderer.invoke('get-sources'),
sendControl: (data) => ipcRenderer.send('remote-control', data),
});

View File

@@ -0,0 +1,386 @@
// ─── 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');
});

View File

@@ -0,0 +1,11 @@
{
"name": "screen-share-signal-server",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"ws": "^8.16.0"
}
}

View File

@@ -0,0 +1,49 @@
const WebSocket = require('ws');
const PORT = process.env.PORT || 9090;
const wss = new WebSocket.Server({ port: PORT });
const peers = new Map();
wss.on('connection', (ws) => {
let peerId = null;
ws.on('message', (raw) => {
let msg;
try {
msg = JSON.parse(raw);
} catch {
return;
}
switch (msg.type) {
case 'register': {
peerId = msg.peerId;
peers.set(peerId, ws);
console.log(`[+] ${peerId} registriert (${peers.size} Peers online)`);
ws.send(JSON.stringify({ type: 'registered', peerId }));
break;
}
case 'offer':
case 'answer':
case 'ice-candidate':
case 'control': {
const target = peers.get(msg.target);
if (target && target.readyState === WebSocket.OPEN) {
target.send(JSON.stringify({ ...msg, from: peerId }));
}
break;
}
}
});
ws.on('close', () => {
if (peerId) {
peers.delete(peerId);
console.log(`[-] ${peerId} getrennt (${peers.size} Peers online)`);
}
});
});
console.log(`Signal-Server laeuft auf ws://0.0.0.0:${PORT}`);