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:
parent
23dd010a95
commit
92e94b1e8a
20
lib/remote-capture-preload.js
Normal file
20
lib/remote-capture-preload.js
Normal 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
124
lib/remote-capture.html
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user