update
This commit is contained in:
163
APP/screen-share/index.html
Normal file
163
APP/screen-share/index.html
Normal 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
75
APP/screen-share/main.js
Normal 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();
|
||||
});
|
||||
16
APP/screen-share/package.json
Normal file
16
APP/screen-share/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
APP/screen-share/preload.js
Normal file
6
APP/screen-share/preload.js
Normal 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),
|
||||
});
|
||||
386
APP/screen-share/renderer.js
Normal file
386
APP/screen-share/renderer.js
Normal 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');
|
||||
});
|
||||
11
APP/screen-share/signal-server/package.json
Normal file
11
APP/screen-share/signal-server/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
49
APP/screen-share/signal-server/server.js
Normal file
49
APP/screen-share/signal-server/server.js
Normal 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}`);
|
||||
Reference in New Issue
Block a user