feat: add remote-capture preload and HTML for WebRTC screen sharing

Adds the hidden BrowserWindow assets for remote desktop streaming:
- lib/remote-capture-preload.js: IPC bridge for desktopCapturer source ID,
  WebRTC signaling relay, input event forwarding, and client count tracking
- lib/remote-capture.html: WebRTC logic handling multiple concurrent clients
  via RTCPeerConnection, stream capture via getUserMedia with desktop source ID,
  and DataChannel input forwarding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-12 06:54:07 +01:00
parent 23dd010a95
commit 92e94b1e8a
2 changed files with 144 additions and 0 deletions

View File

@ -0,0 +1,20 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('capture', {
// Get capture source ID from main process (desktopCapturer runs in main)
getSourceId: () => ipcRenderer.invoke('remote:get-capture-source-id'),
// Signaling: receive offer/ICE from main process (relayed from dashboard)
onSignaling: (callback) => {
ipcRenderer.on('remote:signaling-to-capture', (_event, data) => callback(data));
},
// Signaling: send answer/ICE back to main process (relayed to dashboard)
sendSignaling: (data) => ipcRenderer.send('remote:signaling-from-capture', data),
// Input: forward input events from DataChannel to main process
sendInput: (data) => ipcRenderer.send('remote:input-event', data),
// Notify main process of client connection/disconnection
notifyClientCount: (count) => ipcRenderer.send('remote:client-count', count)
});

124
lib/remote-capture.html Normal file
View File

@ -0,0 +1,124 @@
<!DOCTYPE html>
<html>
<head><title>Remote Capture</title></head>
<body>
<script>
// Maps clientId -> { pc: RTCPeerConnection, dc: RTCDataChannel }
const clients = new Map();
let captureStream = null;
async function getCaptureStream() {
if (captureStream) return captureStream;
// desktopCapturer runs in main process (Electron 33+), we get the source ID via IPC
const sourceId = await window.capture.getSourceId();
if (!sourceId) throw new Error('No capture source ID from main process');
captureStream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId,
minWidth: 1280,
maxWidth: 1920,
minHeight: 720,
maxHeight: 1080,
maxFrameRate: 30
}
}
});
return captureStream;
}
async function handleOffer(clientId, offer, role) {
const stream = await getCaptureStream();
const pc = new RTCPeerConnection({ iceServers: [] });
clients.set(clientId, { pc, role });
// Add video tracks
for (const track of stream.getTracks()) {
pc.addTrack(track, stream);
}
// Handle DataChannel from dashboard (dashboard creates it as offerer)
pc.ondatachannel = (event) => {
const dc = event.channel;
clients.get(clientId).dc = dc;
dc.onmessage = (msg) => {
try {
const input = JSON.parse(msg.data);
input.clientId = clientId;
input.role = role;
window.capture.sendInput(input);
} catch {}
};
};
// ICE candidates
pc.onicecandidate = (event) => {
if (event.candidate) {
window.capture.sendSignaling({
type: 'ice-candidate',
clientId,
candidate: event.candidate
});
}
};
pc.onconnectionstatechange = () => {
if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
removeClient(clientId);
}
};
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
window.capture.sendSignaling({
type: 'answer',
clientId,
answer: pc.localDescription
});
window.capture.notifyClientCount(clients.size);
}
function handleIceCandidate(clientId, candidate) {
const client = clients.get(clientId);
if (client && client.pc) {
client.pc.addIceCandidate(new RTCIceCandidate(candidate)).catch(() => {});
}
}
function removeClient(clientId) {
const client = clients.get(clientId);
if (client) {
if (client.dc) client.dc.close();
client.pc.close();
clients.delete(clientId);
window.capture.notifyClientCount(clients.size);
}
}
// Listen for signaling messages from main process
window.capture.onSignaling((data) => {
switch (data.type) {
case 'offer':
handleOffer(data.clientId, data.offer, data.role).catch(err => {
console.error('Failed to handle offer:', err);
});
break;
case 'ice-candidate':
handleIceCandidate(data.clientId, data.candidate);
break;
case 'client-disconnected':
removeClient(data.clientId);
break;
}
});
</script>
</body>
</html>