Aucune description
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

viewer.html 4.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Remote Session</title>
  6. <style>
  7. body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; }
  8. header { background: #1e293b; padding: 0.6rem 1rem; display: flex; justify-content: space-between; align-items: center; }
  9. #status { font-size: 0.9rem; color: #94a3b8; }
  10. #video { width: 100vw; height: calc(100vh - 48px); background: #020617; object-fit: contain; cursor: crosshair; display: block; outline: none; }
  11. button { padding: 0.5rem 1rem; background: #334155; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
  12. a { color: #3b82f6; }
  13. </style>
  14. </head>
  15. <body>
  16. <header>
  17. <div id="status">Connecting…</div>
  18. <div>
  19. <a href="/">← Console</a>
  20. <button id="endBtn">End session</button>
  21. </div>
  22. </header>
  23. <video id="video" autoplay playsinline muted tabindex="0"></video>
  24. <script>
  25. const params = new URLSearchParams(location.search);
  26. const machineId = params.get('machine');
  27. const machineName = params.get('name') || 'remote PC';
  28. const statusEl = document.getElementById('status');
  29. const video = document.getElementById('video');
  30. let pc, inputChannel, sessionId;
  31. const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
  32. const setStatus = (t) => (statusEl.textContent = t);
  33. ws.onopen = () => {
  34. setStatus(`Requesting access to ${machineName}…`);
  35. ws.send(JSON.stringify({ type: 'viewer-connect', machineId }));
  36. };
  37. ws.onmessage = async (e) => {
  38. const m = JSON.parse(e.data);
  39. switch (m.type) {
  40. case 'session-pending':
  41. sessionId = m.sessionId;
  42. setStatus(`Waiting for ${machineName} to grant consent…`);
  43. break;
  44. case 'session-denied':
  45. setStatus('Consent denied by the remote user.');
  46. break;
  47. case 'session-ready':
  48. setStatus('Consent granted. Establishing connection…');
  49. setupPeer();
  50. break;
  51. case 'offer':
  52. await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
  53. const ans = await pc.createAnswer();
  54. await pc.setLocalDescription(ans);
  55. ws.send(JSON.stringify({ type: 'answer', sessionId, sdp: pc.localDescription }));
  56. break;
  57. case 'ice-candidate':
  58. if (m.candidate && pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate));
  59. break;
  60. case 'session-ended':
  61. setStatus('Session ended.');
  62. video.srcObject = null;
  63. break;
  64. case 'error':
  65. setStatus('Error: ' + m.message);
  66. break;
  67. }
  68. };
  69. function setupPeer() {
  70. pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
  71. inputChannel = pc.createDataChannel('input', { ordered: true });
  72. pc.ontrack = (ev) => {
  73. video.srcObject = ev.streams[0];
  74. setStatus(`Connected to ${machineName} — controlling. Click the screen to send input.`);
  75. video.focus();
  76. };
  77. pc.onicecandidate = (ev) => {
  78. if (ev.candidate) ws.send(JSON.stringify({ type: 'ice-candidate', sessionId, candidate: ev.candidate }));
  79. };
  80. }
  81. // ---- input capture (normalized coords) ----
  82. const send = (o) => { if (inputChannel && inputChannel.readyState === 'open') inputChannel.send(JSON.stringify(o)); };
  83. const rel = (e) => { const r = video.getBoundingClientRect(); return { x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height }; };
  84. let lastMove = 0;
  85. video.addEventListener('mousemove', (e) => { const t = performance.now(); if (t - lastMove < 30) return; lastMove = t; send({ kind: 'mousemove', ...rel(e) }); });
  86. video.addEventListener('mousedown', (e) => { video.focus(); send({ kind: 'mousedown', button: e.button, ...rel(e) }); });
  87. video.addEventListener('mouseup', (e) => send({ kind: 'mouseup', button: e.button, ...rel(e) }));
  88. video.addEventListener('dblclick', (e) => send({ kind: 'dblclick', ...rel(e) }));
  89. video.addEventListener('wheel', (e) => { e.preventDefault(); send({ kind: 'scroll', dx: e.deltaX, dy: e.deltaY }); }, { passive: false });
  90. video.addEventListener('contextmenu', (e) => e.preventDefault());
  91. 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 }); });
  92. video.addEventListener('keyup', (e) => { e.preventDefault(); send({ kind: 'keyup', key: e.key, code: e.code }); });
  93. document.getElementById('endBtn').onclick = () => {
  94. ws.send(JSON.stringify({ type: 'end-session', sessionId }));
  95. setTimeout(() => (location.href = '/'), 300);
  96. };
  97. </script>
  98. </body>
  99. </html>