| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- <!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>
|