Files
BizGaze_Remote/server/public/viewer.html
T

110 lines
4.8 KiB
HTML
Raw Normal View History

2026-06-05 17:29:09 +05:30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Remote Session</title>
<style>
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; }
header { background: #1e293b; padding: 0.6rem 1rem; display: flex; justify-content: space-between; align-items: center; }
#status { font-size: 0.9rem; color: #94a3b8; }
#video { width: 100vw; height: calc(100vh - 48px); background: #020617; object-fit: contain; cursor: crosshair; display: block; outline: none; }
button { padding: 0.5rem 1rem; background: #334155; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
a { color: #3b82f6; }
</style>
<script src="/icons.js"></script>
2026-06-05 17:29:09 +05:30
</head>
<body>
<header>
<div id="status">Connecting…</div>
<div>
<a href="/"><span data-ic="arrowLeft" data-sz="16"></span> Console</a>
2026-06-05 17:29:09 +05:30
<button id="endBtn">End session</button>
</div>
</header>
<video id="video" autoplay playsinline muted tabindex="0"></video>
<script>
const params = new URLSearchParams(location.search);
const machineId = params.get('machine');
const machineName = params.get('name') || 'remote PC';
const statusEl = document.getElementById('status');
const video = document.getElementById('video');
let pc, inputChannel, sessionId;
const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
const setStatus = (t) => (statusEl.textContent = t);
ws.onopen = () => {
setStatus(`Requesting access to ${machineName}…`);
ws.send(JSON.stringify({ type: 'viewer-connect', machineId }));
};
ws.onmessage = async (e) => {
const m = JSON.parse(e.data);
switch (m.type) {
case 'session-pending':
sessionId = m.sessionId;
setStatus(`Waiting for ${machineName} to grant consent…`);
break;
case 'session-denied':
setStatus('Consent denied by the remote user.');
break;
case 'session-ready':
setStatus('Consent granted. Establishing connection…');
setupPeer();
break;
case 'offer':
await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
const ans = await pc.createAnswer();
await pc.setLocalDescription(ans);
ws.send(JSON.stringify({ type: 'answer', sessionId, sdp: pc.localDescription }));
break;
case 'ice-candidate':
if (m.candidate && pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate));
break;
case 'session-ended':
setStatus('Session ended.');
video.srcObject = null;
break;
case 'error':
setStatus('Error: ' + m.message);
break;
}
};
function setupPeer() {
pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
inputChannel = pc.createDataChannel('input', { ordered: true });
pc.ontrack = (ev) => {
video.srcObject = ev.streams[0];
setStatus(`Connected to ${machineName} — controlling. Click the screen to send input.`);
video.focus();
};
pc.onicecandidate = (ev) => {
if (ev.candidate) ws.send(JSON.stringify({ type: 'ice-candidate', sessionId, candidate: ev.candidate }));
};
}
// ---- input capture (normalized coords) ----
const send = (o) => { if (inputChannel && inputChannel.readyState === 'open') inputChannel.send(JSON.stringify(o)); };
const rel = (e) => { const r = video.getBoundingClientRect(); return { x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height }; };
let lastMove = 0;
video.addEventListener('mousemove', (e) => { const t = performance.now(); if (t - lastMove < 30) return; lastMove = t; send({ kind: 'mousemove', ...rel(e) }); });
video.addEventListener('mousedown', (e) => { video.focus(); send({ kind: 'mousedown', button: e.button, ...rel(e) }); });
video.addEventListener('mouseup', (e) => send({ kind: 'mouseup', button: e.button, ...rel(e) }));
video.addEventListener('dblclick', (e) => send({ kind: 'dblclick', ...rel(e) }));
video.addEventListener('wheel', (e) => { e.preventDefault(); send({ kind: 'scroll', dx: e.deltaX, dy: e.deltaY }); }, { passive: false });
video.addEventListener('contextmenu', (e) => e.preventDefault());
video.addEventListener('keydown', (e) => { e.preventDefault(); send({ kind: 'keydown', key: e.key, code: e.code, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey }); });
video.addEventListener('keyup', (e) => { e.preventDefault(); send({ kind: 'keyup', key: e.key, code: e.code }); });
document.getElementById('endBtn').onclick = () => {
ws.send(JSON.stringify({ type: 'end-session', sessionId }));
setTimeout(() => (location.href = '/'), 300);
};
</script>
<script>(function(){var s=document.createElement('style');s.textContent='.ic{display:inline-block;vertical-align:middle}';document.head.appendChild(s);document.querySelectorAll('[data-ic]').forEach(function(e){e.insertAdjacentHTML('afterbegin',window.ic(e.getAttribute('data-ic'),+e.getAttribute('data-sz')||16));});})();</script>
2026-06-05 17:29:09 +05:30
</body>
</html>