Multi-Hoster-Upload/lib/remote-capture.html
Administrator c9d038d588 debug: send capture errors back via signaling channel
If getCaptureStream fails, send error back through WebSocket so it
appears in proxy logs for diagnosis.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 08:49:07 +01:00

155 lines
4.5 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();
window.capture.log('getSourceId returned:', sourceId || 'NULL');
if (!sourceId) throw new Error('No capture source ID from main process');
try {
captureStream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId,
minWidth: 1280,
maxWidth: 1920,
minHeight: 720,
maxHeight: 1080,
maxFrameRate: 30
}
}
});
const tracks = captureStream.getTracks();
window.capture.log('getUserMedia OK, tracks:', tracks.length, tracks.map(t => `${t.kind}:${t.readyState}`).join(','));
return captureStream;
} catch (err) {
window.capture.log('getUserMedia FAILED:', err.message);
throw err;
}
}
async function handleOffer(clientId, offer, role) {
window.capture.log('handleOffer called for', clientId);
let stream;
try {
stream = await getCaptureStream();
} catch (err) {
window.capture.log('FATAL: getCaptureStream failed:', err.message);
// Send diagnostic back to dashboard
window.capture.sendSignaling({ type: 'capture-error', clientId, error: err.message });
return;
}
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
clients.set(clientId, { pc, role });
// Add video tracks
const tracks = stream.getTracks();
window.capture.log('Adding', tracks.length, 'tracks to peer connection');
for (const track of tracks) {
window.capture.log('addTrack:', track.kind, track.label, track.readyState);
pc.addTrack(track, stream);
}
window.capture.log('Senders after addTrack:', pc.getSenders().length);
// 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 — serialize to plain object (WebRTC objects don't survive IPC)
pc.onicecandidate = (event) => {
if (event.candidate) {
window.capture.sendSignaling({
type: 'ice-candidate',
clientId,
candidate: {
candidate: event.candidate.candidate,
sdpMid: event.candidate.sdpMid,
sdpMLineIndex: event.candidate.sdpMLineIndex,
usernameFragment: event.candidate.usernameFragment
}
});
}
};
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);
// Serialize to plain object (RTCSessionDescription doesn't survive IPC)
window.capture.sendSignaling({
type: 'answer',
clientId,
answer: { type: pc.localDescription.type, sdp: pc.localDescription.sdp }
});
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);
window.capture.sendSignaling({ type: 'error', clientId: data.clientId, error: err.message });
});
break;
case 'ice-candidate':
handleIceCandidate(data.clientId, data.candidate);
break;
case 'client-disconnected':
removeClient(data.clientId);
break;
}
});
</script>
</body>
</html>