127 lines
3.2 KiB
HTML
127 lines
3.2 KiB
HTML
<!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: [{ urls: 'stun:stun.l.google.com:19302' }]
|
|
});
|
|
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>
|