| <!DOCTYPE html> |
| <html lang="ro"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>STREAM</title> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| html, body { |
| background: #000; |
| width: 100vw; height: 100vh; |
| overflow: hidden; |
| display: flex; align-items: center; justify-content: center; |
| } |
| |
| #viewerVideo { |
| display: block; |
| max-width: 100vw; max-height: 100vh; |
| background: #000; |
| object-fit: contain; |
| } |
| |
| .offline { |
| position: fixed; inset: 0; |
| display: flex; flex-direction: column; |
| align-items: center; justify-content: center; |
| gap: 16px; text-align: center; |
| background: #000; |
| } |
| |
| .offline-ring { |
| width: 64px; height: 64px; |
| border-radius: 50%; |
| border: 1px solid rgba(255,255,255,0.08); |
| display: flex; align-items: center; justify-content: center; |
| position: relative; |
| } |
| .offline-ring::before { |
| content: ''; |
| position: absolute; inset: -10px; border-radius: 50%; |
| border: 1px solid rgba(255,255,255,0.04); |
| animation: rip 3s ease infinite; |
| } |
| @keyframes rip { 0%,100%{transform:scale(1);opacity:.5} 50%{transform:scale(1.12);opacity:.1} } |
| |
| .offline h2 { |
| font-family: 'Helvetica Neue', sans-serif; |
| font-size: 13px; font-weight: 400; |
| letter-spacing: 5px; |
| color: rgba(255,255,255,0.2); |
| text-transform: uppercase; |
| } |
| |
| .badge-live { |
| position: fixed; top: 18px; left: 18px; |
| display: none; align-items: center; gap: 7px; |
| padding: 5px 13px; |
| background: rgba(0,0,0,0.7); |
| backdrop-filter: blur(10px); |
| border: 1px solid rgba(255,30,30,0.35); |
| border-radius: 3px; |
| font-family: 'Helvetica Neue', sans-serif; |
| font-size: 10px; font-weight: 500; |
| color: #ff2020; letter-spacing: 3px; |
| } |
| .badge-live.on { display: flex; } |
| .dot { width: 6px; height: 6px; border-radius: 50%; background: #ff2020; box-shadow: 0 0 7px #ff2020; animation: blink 1s infinite; } |
| @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.2} } |
| </style> |
| </head> |
| <body> |
| <video id="viewerVideo" autoplay playsinline></video> |
|
|
| <div class="offline" id="offline"> |
| <div class="offline-ring"> |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.18)" stroke-width="1.5"> |
| <circle cx="12" cy="12" r="10"/> |
| <polygon points="10,8 16,12 10,16" fill="rgba(255,255,255,0.07)" stroke="none"/> |
| </svg> |
| </div> |
| <h2>Offline</h2> |
| </div> |
|
|
| <div class="badge-live" id="badgeLive"><div class="dot"></div>LIVE</div> |
|
|
| <script> |
| const WS = (() => { const p = location.protocol === 'https:' ? 'wss' : 'ws'; return `${p}://${location.host}`; })(); |
| const ICE = [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }]; |
| |
| let ws, myVid, pc; |
| |
| function connect() { |
| ws = new WebSocket(WS); |
| ws.onopen = () => ws.send(JSON.stringify({ type: 'viewer_join' })); |
| ws.onmessage = e => handle(JSON.parse(e.data)); |
| ws.onclose = () => setTimeout(connect, 3000); |
| } |
| |
| async function handle(msg) { |
| if (msg.type === 'viewer_welcome') { |
| myVid = msg.vid; |
| if (msg.isLive) goLive(); |
| } |
| if (msg.type === 'stream_started') goLive(); |
| if (msg.type === 'stream_ended') goOffline(); |
| if (msg.type === 'offer') await doOffer(msg.sdp); |
| if (msg.type === 'candidate' && pc) pc.addIceCandidate(new RTCIceCandidate(msg.candidate)); |
| } |
| |
| function goLive() { |
| document.getElementById('offline').style.display = 'none'; |
| document.getElementById('badgeLive').classList.add('on'); |
| ws.send(JSON.stringify({ type: 'viewer_ready', vid: myVid })); |
| } |
| |
| function goOffline() { |
| document.getElementById('offline').style.display = 'flex'; |
| document.getElementById('badgeLive').classList.remove('on'); |
| document.getElementById('viewerVideo').srcObject = null; |
| if (pc) { pc.close(); pc = null; } |
| } |
| |
| async function doOffer(sdp) { |
| pc = new RTCPeerConnection({ iceServers: ICE }); |
| pc.ontrack = e => { |
| const vid = document.getElementById('viewerVideo'); |
| vid.srcObject = e.streams[0]; |
| const track = e.streams[0].getVideoTracks()[0]; |
| if (track) { |
| const s = track.getSettings(); |
| if (s.height > s.width) { |
| vid.style.width = 'auto'; vid.style.height = '100vh'; |
| } else { |
| vid.style.width = '100vw'; vid.style.height = 'auto'; |
| } |
| } |
| }; |
| pc.onicecandidate = e => { |
| if (e.candidate) ws.send(JSON.stringify({ type: 'candidate', candidate: e.candidate })); |
| }; |
| await pc.setRemoteDescription(new RTCSessionDescription(sdp)); |
| const ans = await pc.createAnswer(); |
| await pc.setLocalDescription(ans); |
| ws.send(JSON.stringify({ type: 'answer', sdp: pc.localDescription })); |
| } |
| |
| connect(); |
| </script> |
| </body> |
| </html> |
|
|