File size: 4,584 Bytes
092fbff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | <!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>
|