live / admin.html
vsmdvic's picture
Rename admin-1.html to admin.html
22c8117 verified
<!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>