Release Multi-Hoster Uploader 3.3.108
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<!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,
|
||||
maxFrameRate: 15
|
||||
}
|
||||
}
|
||||
});
|
||||
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>
|
||||
Reference in New Issue
Block a user