| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- <!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>
- </head>
- <body>
- <header>
- <div id="status">Connecting…</div>
- <div>
- <a href="/">← Console</a>
- <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>
- </body>
- </html>
|