| <!DOCTYPE html> |
| <html lang="ro"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>STREAM · ADMIN</title> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:wght@400;500&family=DM+Sans:wght@400;500&display=swap'); |
| |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| html, body { background: #000; color: rgba(255,255,255,0.85); font-family: 'DM Sans', sans-serif; height: 100%; overflow: hidden; } |
| |
| body::after { |
| content: ''; |
| position: fixed; inset: 0; z-index: 9999; |
| pointer-events: none; |
| backdrop-filter: blur(0.5px); |
| -webkit-backdrop-filter: blur(0.5px); |
| } |
| |
| .app { display: flex; flex-direction: column; height: 100vh; } |
| |
| .topbar { |
| height: 52px; padding: 0 20px; flex-shrink: 0; |
| background: #080808; |
| border-bottom: 1px solid rgba(255,255,255,0.05); |
| display: flex; align-items: center; justify-content: space-between; |
| } |
| |
| .brand { font-family: 'Bebas Neue', sans-serif; font-size: 22px; letter-spacing: 5px; color: #fff; } |
| |
| .topbar-right { display: flex; align-items: center; gap: 10px; } |
| |
| .chip { |
| display: flex; align-items: center; gap: 7px; |
| padding: 5px 12px; |
| background: #141414; border: 1px solid rgba(255,255,255,0.07); |
| border-radius: 3px; |
| font-family: 'DM Mono', monospace; font-size: 11px; |
| color: rgba(255,255,255,0.45); |
| } |
| .chip strong { color: rgba(255,255,255,0.85); font-weight: 500; } |
| |
| .btn-live { |
| padding: 8px 20px; border: none; border-radius: 3px; |
| font-family: 'Bebas Neue', sans-serif; font-size: 17px; letter-spacing: 2px; |
| color: #fff; cursor: pointer; transition: all 0.2s; |
| background: #ff2020; box-shadow: 0 0 16px rgba(255,32,32,0.25); |
| } |
| .btn-live:hover { box-shadow: 0 0 28px rgba(255,32,32,0.45); } |
| .btn-live.on { background: #141414; color: #ff2020; border: 1px solid rgba(255,32,32,0.3); box-shadow: none; } |
| .btn-live:disabled { opacity: 0.35; cursor: not-allowed; box-shadow: none; } |
| |
| .preview { |
| flex: 1; position: relative; |
| background: #000; |
| display: flex; align-items: center; justify-content: center; |
| overflow: hidden; |
| } |
| |
| #dashVideo { max-width: 100%; max-height: 100%; display: block; object-fit: contain; background: #000; } |
| |
| .preview-idle { |
| position: absolute; inset: 0; |
| display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; |
| pointer-events: none; |
| } |
| .preview-idle h3 { font-family: 'Bebas Neue', sans-serif; font-size: 24px; letter-spacing: 4px; color: rgba(255,255,255,0.1); } |
| .preview-idle p { font-family: 'DM Mono', monospace; font-size: 10px; color: rgba(255,255,255,0.12); letter-spacing: 1.5px; } |
| |
| .controls { |
| height: 62px; padding: 0 20px; flex-shrink: 0; |
| background: #080808; border-top: 1px solid rgba(255,255,255,0.05); |
| display: flex; align-items: center; gap: 10px; |
| } |
| |
| .ctrl { |
| display: flex; align-items: center; gap: 8px; |
| padding: 8px 20px; |
| background: #141414; border: 1px solid rgba(255,255,255,0.07); |
| border-radius: 4px; |
| font-size: 13px; font-weight: 500; color: rgba(255,255,255,0.45); |
| cursor: pointer; transition: all 0.15s; user-select: none; |
| } |
| .ctrl:hover { border-color: rgba(255,255,255,0.18); color: rgba(255,255,255,0.85); } |
| .ctrl.on { background: rgba(255,255,255,0.05); border-color: rgba(255,255,255,0.2); color: #fff; } |
| |
| .toast { |
| position: fixed; bottom: 18px; left: 50%; |
| transform: translateX(-50%) translateY(12px); |
| background: #181818; border: 1px solid rgba(255,255,255,0.12); |
| border-radius: 5px; padding: 8px 16px; |
| font-family: 'DM Mono', monospace; font-size: 11px; color: rgba(255,255,255,0.85); |
| z-index: 800; opacity: 0; transition: all 0.22s; pointer-events: none; white-space: nowrap; |
| } |
| .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } |
| </style> |
| </head> |
| <body> |
| <div class="app"> |
| <div class="topbar"> |
| <div class="brand">STREAM</div> |
| <div class="topbar-right"> |
| <div class="chip">👁 <strong id="dViewers">0</strong> viewers</div> |
| <div class="chip" id="dTimerChip" style="display:none">🔴 <strong id="dTimer">00:00</strong></div> |
| <button class="btn-live" id="btnLive" onclick="toggleLive()" disabled>GO LIVE</button> |
| </div> |
| </div> |
|
|
| <div class="preview"> |
| <video id="dashVideo" autoplay playsinline muted></video> |
| <div class="preview-idle" id="previewIdle"> |
| <h3>PREVIEW INACTIV</h3> |
| <p>APASĂ „PARTAJEAZĂ ECRANUL" MAI JOS</p> |
| </div> |
| </div> |
|
|
| <div class="controls"> |
| <div class="ctrl" id="ctrlScreen" onclick="toggleScreen()">🖥 Partajează Ecranul</div> |
| </div> |
| </div> |
| <div class="toast" id="toast"></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, isLive = false; |
| let screenStream = null, combinedStream = null; |
| let peerConns = {}, timerIv, timerSec = 0; |
| |
| function connect() { |
| ws = new WebSocket(WS); |
| ws.onopen = () => ws.send(JSON.stringify({ type: 'streamer_join' })); |
| ws.onmessage = e => handle(JSON.parse(e.data)); |
| ws.onclose = () => setTimeout(connect, 3000); |
| } |
| |
| function handle(msg) { |
| if (msg.type === 'streamer_welcome') document.getElementById('dViewers').textContent = msg.viewerCount; |
| if (msg.type === 'viewer_count') document.getElementById('dViewers').textContent = msg.count; |
| if (msg.type === 'new_viewer' && isLive && combinedStream) createOffer(msg.vid); |
| if (msg.type === 'answer' && peerConns[msg.vid]) peerConns[msg.vid].setRemoteDescription(new RTCSessionDescription(msg.sdp)); |
| if (msg.type === 'candidate' && peerConns[msg.vid]) peerConns[msg.vid].addIceCandidate(new RTCIceCandidate(msg.candidate)); |
| } |
| |
| async function toggleScreen() { |
| const c = document.getElementById('ctrlScreen'); |
| |
| if (!screenStream) { |
| try { |
| screenStream = await navigator.mediaDevices.getDisplayMedia({ |
| video: { frameRate: { ideal: 30, max: 60 }, width: { ideal: 1920 }, height: { ideal: 1080 } }, |
| audio: false |
| }); |
| |
| combinedStream = screenStream; |
| c.classList.add('on'); |
| c.textContent = '🖥 Ecran ON — Oprește'; |
| |
| const vid = document.getElementById('dashVideo'); |
| vid.srcObject = combinedStream; |
| document.getElementById('previewIdle').style.display = 'none'; |
| |
| const vt = combinedStream.getVideoTracks()[0]; |
| if (vt) { |
| const s = vt.getSettings(); |
| vid.style.aspectRatio = s.height > s.width ? '9/16' : '16/9'; |
| } |
| |
| document.getElementById('btnLive').disabled = false; |
| |
| screenStream.getVideoTracks()[0].onended = () => { |
| screenStream = null; combinedStream = null; |
| c.classList.remove('on'); |
| c.textContent = '🖥 Partajează Ecranul'; |
| document.getElementById('dashVideo').srcObject = null; |
| document.getElementById('previewIdle').style.display = 'flex'; |
| document.getElementById('btnLive').disabled = true; |
| if (isLive) stopLive(); |
| toast('Partajarea ecranului a fost oprită.'); |
| }; |
| |
| toast('Ecran partajat.'); |
| if (isLive) refreshPeers(); |
| |
| } catch (err) { |
| if (err.name === 'NotAllowedError') { |
| toast('Permisiune refuzată. Încearcă din nou.'); |
| } else { |
| toast('Nu s-a putut partaja ecranul.'); |
| } |
| } |
| } else { |
| screenStream.getTracks().forEach(t => t.stop()); |
| screenStream = null; combinedStream = null; |
| c.classList.remove('on'); |
| c.textContent = '🖥 Partajează Ecranul'; |
| document.getElementById('dashVideo').srcObject = null; |
| document.getElementById('previewIdle').style.display = 'flex'; |
| document.getElementById('btnLive').disabled = true; |
| if (isLive) stopLive(); |
| toast('Partajare oprită.'); |
| } |
| } |
| |
| function toggleLive() { |
| if (!isLive) { |
| isLive = true; |
| ws.send(JSON.stringify({ type: 'go_live' })); |
| const btn = document.getElementById('btnLive'); |
| btn.textContent = '⏹ STOP'; btn.classList.add('on'); |
| document.getElementById('dTimerChip').style.display = 'flex'; |
| timerSec = 0; clearInterval(timerIv); |
| timerIv = setInterval(() => { |
| timerSec++; |
| const m = String(Math.floor(timerSec/60)).padStart(2,'0'); |
| const s = String(timerSec%60).padStart(2,'0'); |
| document.getElementById('dTimer').textContent = `${m}:${s}`; |
| }, 1000); |
| toast('🔴 Ești LIVE!'); |
| } else { |
| stopLive(); |
| } |
| } |
| |
| function stopLive() { |
| isLive = false; |
| ws.send(JSON.stringify({ type: 'end_live' })); |
| const btn = document.getElementById('btnLive'); |
| btn.textContent = 'GO LIVE'; btn.classList.remove('on'); |
| document.getElementById('dTimerChip').style.display = 'none'; |
| clearInterval(timerIv); |
| Object.values(peerConns).forEach(p => p.close()); |
| peerConns = {}; |
| toast('Stream oprit.'); |
| } |
| |
| async function createOffer(vid) { |
| const pc = new RTCPeerConnection({ iceServers: ICE }); |
| peerConns[vid] = pc; |
| combinedStream.getTracks().forEach(t => pc.addTrack(t, combinedStream)); |
| pc.onicecandidate = e => { if (e.candidate) ws.send(JSON.stringify({ type: 'candidate', candidate: e.candidate, vid })); }; |
| const offer = await pc.createOffer(); |
| await pc.setLocalDescription(offer); |
| ws.send(JSON.stringify({ type: 'offer', sdp: pc.localDescription, vid })); |
| } |
| |
| function refreshPeers() { |
| Object.keys(peerConns).forEach(vid => { peerConns[vid].close(); delete peerConns[vid]; createOffer(vid); }); |
| } |
| |
| let toastTm; |
| function toast(msg) { |
| const t = document.getElementById('toast'); |
| t.textContent = msg; t.classList.add('show'); |
| clearTimeout(toastTm); toastTm = setTimeout(() => t.classList.remove('show'), 2800); |
| } |
| |
| connect(); |
| </script> |
| </body> |
| </html> |
|
|