103 lines
5.6 KiB
HTML
103 lines
5.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Browser Host — Remote Access</title>
|
|
<style>
|
|
body { font-family: system-ui, sans-serif; background:#0f172a; color:#e2e8f0; margin:0; padding:1.5rem; }
|
|
.card { max-width:560px; margin:0 auto; background:#1e293b; border-radius:12px; padding:1.5rem; }
|
|
h1 { font-size:1.15rem; margin:0 0 1rem; }
|
|
input { width:100%; padding:.7rem; border-radius:8px; border:1px solid #334155; background:#0f172a; color:#e2e8f0; margin:.3rem 0; }
|
|
button { padding:.7rem 1.3rem; background:#3b82f6; color:#fff; border:none; border-radius:8px; font-weight:600; cursor:pointer; }
|
|
.status { background:#0f172a; padding:.7rem 1rem; border-radius:8px; margin:1rem 0; font-size:.9rem; }
|
|
.status.on { background:#14532d; } .status.warn { background:#7c2d12; }
|
|
.consent { border:1px solid #3b82f6; border-radius:10px; padding:1rem; margin-top:1rem; }
|
|
.grant { background:#22c55e; color:#052e16; } .deny { background:#ef4444; margin-left:.5rem; }
|
|
.muted { color:#94a3b8; font-size:.82rem; }
|
|
#log { font-family:monospace; font-size:.72rem; color:#64748b; height:120px; overflow-y:auto; margin-top:1rem; background:#020617; padding:.6rem; border-radius:8px; }
|
|
.indicator { position:fixed; bottom:0; left:0; right:0; background:#b91c1c; color:#fff; text-align:center; padding:.4rem; font-size:.85rem; display:none; }
|
|
.indicator.show { display:block; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<h1>🖥️ Browser Host (no install)</h1>
|
|
<p class="muted">Shares this screen with a technician. Paste the enroll token from the console and click Go online.</p>
|
|
<input id="token" placeholder="enroll token">
|
|
<button id="goBtn">Go online</button>
|
|
<div id="status" class="status">Idle.</div>
|
|
<div id="consentBox"></div>
|
|
<div id="log"></div>
|
|
</div>
|
|
<div id="indicator" class="indicator">● A technician is viewing this screen</div>
|
|
|
|
<script>
|
|
const statusEl = document.getElementById('status');
|
|
const consentBox = document.getElementById('consentBox');
|
|
const indicator = document.getElementById('indicator');
|
|
const logEl = document.getElementById('log');
|
|
const log = (t) => { const d=document.createElement('div'); d.textContent=t; logEl.appendChild(d); logEl.scrollTop=logEl.scrollHeight; };
|
|
const setStatus = (t,c='') => { statusEl.textContent=t; statusEl.className='status '+c; };
|
|
|
|
let ws, pc, localStream, sessionId;
|
|
|
|
document.getElementById('goBtn').onclick = () => {
|
|
const token = document.getElementById('token').value.trim();
|
|
if (!token) return setStatus('Enter the enroll token first.', 'warn');
|
|
connect(token);
|
|
};
|
|
|
|
function connect(token) {
|
|
setStatus('Connecting to server…');
|
|
ws = new WebSocket((location.protocol==='https:'?'wss://':'ws://') + location.host + '/ws');
|
|
ws.onopen = () => ws.send(JSON.stringify({ type:'agent-hello', enrollToken: token }));
|
|
ws.onmessage = (e) => handle(JSON.parse(e.data));
|
|
ws.onclose = () => setStatus('Disconnected.', 'warn');
|
|
}
|
|
|
|
async function handle(m) {
|
|
switch (m.type) {
|
|
case 'agent-registered': setStatus(`Online as "${m.name}". Waiting for a session.`, 'on'); log('registered: '+m.name); break;
|
|
case 'session-request': m.unattended ? grant(m.sessionId) : showConsent(m); break;
|
|
case 'start-stream': sessionId = m.sessionId; await startStreaming(); break;
|
|
case 'answer': if (pc) await pc.setRemoteDescription(new RTCSessionDescription(m.sdp)); break;
|
|
case 'ice-candidate': if (m.candidate && pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); break;
|
|
case 'session-ended': teardown(); break;
|
|
case 'error': setStatus('Server: '+m.message, 'warn'); break;
|
|
}
|
|
}
|
|
|
|
function showConsent(m) {
|
|
consentBox.innerHTML = `<div class="consent"><b>${esc(m.technician)}</b> wants to view this screen.
|
|
<div style="margin-top:.6rem"><button class="grant" id="g">Allow</button><button class="deny" id="d">Deny</button></div></div>`;
|
|
document.getElementById('g').onclick = () => { consentBox.innerHTML=''; grant(m.sessionId); };
|
|
document.getElementById('d').onclick = () => { consentBox.innerHTML=''; ws.send(JSON.stringify({type:'consent',sessionId:m.sessionId,granted:false})); };
|
|
}
|
|
const grant = (sid) => ws.send(JSON.stringify({ type:'consent', sessionId:sid, granted:true }));
|
|
|
|
async function startStreaming() {
|
|
setStatus('Sharing screen…', 'on'); indicator.classList.add('show');
|
|
try { localStream = await navigator.mediaDevices.getDisplayMedia({ video:{frameRate:{ideal:30}}, audio:false }); }
|
|
catch (err) { log('getDisplayMedia failed: '+err.message); setStatus('Screen capture cancelled/failed.', 'warn'); return; }
|
|
pc = new RTCPeerConnection({ iceServers:[{urls:'stun:stun.l.google.com:19302'}] });
|
|
localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
|
|
pc.ondatachannel = (ev) => { ev.channel.onmessage = (msg) => { let e; try{e=JSON.parse(msg.data)}catch{return} if(e.kind!=='mousemove') log('remote input: '+e.kind+' '+(e.key||'')); }; };
|
|
pc.onicecandidate = (ev) => { if (ev.candidate) ws.send(JSON.stringify({type:'ice-candidate',sessionId,candidate:ev.candidate})); };
|
|
const offer = await pc.createOffer(); await pc.setLocalDescription(offer);
|
|
ws.send(JSON.stringify({ type:'offer', sessionId, sdp: pc.localDescription }));
|
|
log('sent offer');
|
|
localStream.getVideoTracks()[0].onended = () => teardown();
|
|
}
|
|
|
|
function teardown() {
|
|
indicator.classList.remove('show');
|
|
if (localStream) { localStream.getTracks().forEach(t=>t.stop()); localStream=null; }
|
|
if (pc) { pc.close(); pc=null; }
|
|
setStatus('Session ended. Still online, waiting.', 'on');
|
|
}
|
|
function esc(s){return String(s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
|
</script>
|
|
</body>
|
|
</html>
|