Нет описания
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Browser Host — Remote Access</title>
  7. <style>
  8. body { font-family: system-ui, sans-serif; background:#0f172a; color:#e2e8f0; margin:0; padding:1.5rem; }
  9. .card { max-width:560px; margin:0 auto; background:#1e293b; border-radius:12px; padding:1.5rem; }
  10. h1 { font-size:1.15rem; margin:0 0 1rem; }
  11. input { width:100%; padding:.7rem; border-radius:8px; border:1px solid #334155; background:#0f172a; color:#e2e8f0; margin:.3rem 0; }
  12. button { padding:.7rem 1.3rem; background:#3b82f6; color:#fff; border:none; border-radius:8px; font-weight:600; cursor:pointer; }
  13. .status { background:#0f172a; padding:.7rem 1rem; border-radius:8px; margin:1rem 0; font-size:.9rem; }
  14. .status.on { background:#14532d; } .status.warn { background:#7c2d12; }
  15. .consent { border:1px solid #3b82f6; border-radius:10px; padding:1rem; margin-top:1rem; }
  16. .grant { background:#22c55e; color:#052e16; } .deny { background:#ef4444; margin-left:.5rem; }
  17. .muted { color:#94a3b8; font-size:.82rem; }
  18. #log { font-family:monospace; font-size:.72rem; color:#64748b; height:120px; overflow-y:auto; margin-top:1rem; background:#020617; padding:.6rem; border-radius:8px; }
  19. .indicator { position:fixed; bottom:0; left:0; right:0; background:#b91c1c; color:#fff; text-align:center; padding:.4rem; font-size:.85rem; display:none; }
  20. .indicator.show { display:block; }
  21. </style>
  22. </head>
  23. <body>
  24. <div class="card">
  25. <h1>🖥️ Browser Host (no install)</h1>
  26. <p class="muted">Shares this screen with a technician. Paste the enroll token from the console and click Go online.</p>
  27. <input id="token" placeholder="enroll token">
  28. <button id="goBtn">Go online</button>
  29. <div id="status" class="status">Idle.</div>
  30. <div id="consentBox"></div>
  31. <div id="log"></div>
  32. </div>
  33. <div id="indicator" class="indicator">● A technician is viewing this screen</div>
  34. <script>
  35. const statusEl = document.getElementById('status');
  36. const consentBox = document.getElementById('consentBox');
  37. const indicator = document.getElementById('indicator');
  38. const logEl = document.getElementById('log');
  39. const log = (t) => { const d=document.createElement('div'); d.textContent=t; logEl.appendChild(d); logEl.scrollTop=logEl.scrollHeight; };
  40. const setStatus = (t,c='') => { statusEl.textContent=t; statusEl.className='status '+c; };
  41. let ws, pc, localStream, sessionId;
  42. document.getElementById('goBtn').onclick = () => {
  43. const token = document.getElementById('token').value.trim();
  44. if (!token) return setStatus('Enter the enroll token first.', 'warn');
  45. connect(token);
  46. };
  47. function connect(token) {
  48. setStatus('Connecting to server…');
  49. ws = new WebSocket((location.protocol==='https:'?'wss://':'ws://') + location.host + '/ws');
  50. ws.onopen = () => ws.send(JSON.stringify({ type:'agent-hello', enrollToken: token }));
  51. ws.onmessage = (e) => handle(JSON.parse(e.data));
  52. ws.onclose = () => setStatus('Disconnected.', 'warn');
  53. }
  54. async function handle(m) {
  55. switch (m.type) {
  56. case 'agent-registered': setStatus(`Online as "${m.name}". Waiting for a session.`, 'on'); log('registered: '+m.name); break;
  57. case 'session-request': m.unattended ? grant(m.sessionId) : showConsent(m); break;
  58. case 'start-stream': sessionId = m.sessionId; await startStreaming(); break;
  59. case 'answer': if (pc) await pc.setRemoteDescription(new RTCSessionDescription(m.sdp)); break;
  60. case 'ice-candidate': if (m.candidate && pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); break;
  61. case 'session-ended': teardown(); break;
  62. case 'error': setStatus('Server: '+m.message, 'warn'); break;
  63. }
  64. }
  65. function showConsent(m) {
  66. consentBox.innerHTML = `<div class="consent"><b>${esc(m.technician)}</b> wants to view this screen.
  67. <div style="margin-top:.6rem"><button class="grant" id="g">Allow</button><button class="deny" id="d">Deny</button></div></div>`;
  68. document.getElementById('g').onclick = () => { consentBox.innerHTML=''; grant(m.sessionId); };
  69. document.getElementById('d').onclick = () => { consentBox.innerHTML=''; ws.send(JSON.stringify({type:'consent',sessionId:m.sessionId,granted:false})); };
  70. }
  71. const grant = (sid) => ws.send(JSON.stringify({ type:'consent', sessionId:sid, granted:true }));
  72. async function startStreaming() {
  73. setStatus('Sharing screen…', 'on'); indicator.classList.add('show');
  74. try { localStream = await navigator.mediaDevices.getDisplayMedia({ video:{frameRate:{ideal:30}}, audio:false }); }
  75. catch (err) { log('getDisplayMedia failed: '+err.message); setStatus('Screen capture cancelled/failed.', 'warn'); return; }
  76. pc = new RTCPeerConnection({ iceServers:[{urls:'stun:stun.l.google.com:19302'}] });
  77. localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
  78. 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||'')); }; };
  79. pc.onicecandidate = (ev) => { if (ev.candidate) ws.send(JSON.stringify({type:'ice-candidate',sessionId,candidate:ev.candidate})); };
  80. const offer = await pc.createOffer(); await pc.setLocalDescription(offer);
  81. ws.send(JSON.stringify({ type:'offer', sessionId, sdp: pc.localDescription }));
  82. log('sent offer');
  83. localStream.getVideoTracks()[0].onended = () => teardown();
  84. }
  85. function teardown() {
  86. indicator.classList.remove('show');
  87. if (localStream) { localStream.getTracks().forEach(t=>t.stop()); localStream=null; }
  88. if (pc) { pc.close(); pc=null; }
  89. setStatus('Session ended. Still online, waiting.', 'on');
  90. }
  91. function esc(s){return String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
  92. </script>
  93. </body>
  94. </html>