Multi-Hoster-Upload/lib/remote-capture.html
Administrator 82b597506b debug: add IPC logging from capture window to main process
Capture window logs now forwarded to main process via IPC to diagnose
why video tracks are missing from the WebRTC answer SDP.

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

146 lines
4.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();
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);
const stream = await getCaptureStream();
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) {
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>