vsmdvic commited on
Commit
092fbff
·
verified ·
1 Parent(s): aa89102

Upload 3 files

Browse files
Files changed (3) hide show
  1. admin.html +261 -0
  2. index-6.html +150 -0
  3. server.js +18 -32
admin.html ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ro">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>STREAM · ADMIN</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:wght@400;500&family=DM+Sans:wght@400;500&display=swap');
9
+
10
+ * { margin: 0; padding: 0; box-sizing: border-box; }
11
+ html, body { background: #000; color: rgba(255,255,255,0.85); font-family: 'DM Sans', sans-serif; height: 100%; overflow: hidden; }
12
+
13
+ body::after {
14
+ content: '';
15
+ position: fixed; inset: 0; z-index: 9999;
16
+ pointer-events: none;
17
+ backdrop-filter: blur(0.5px);
18
+ -webkit-backdrop-filter: blur(0.5px);
19
+ }
20
+
21
+ .app { display: flex; flex-direction: column; height: 100vh; }
22
+
23
+ .topbar {
24
+ height: 52px; padding: 0 20px; flex-shrink: 0;
25
+ background: #080808;
26
+ border-bottom: 1px solid rgba(255,255,255,0.05);
27
+ display: flex; align-items: center; justify-content: space-between;
28
+ }
29
+
30
+ .brand { font-family: 'Bebas Neue', sans-serif; font-size: 22px; letter-spacing: 5px; color: #fff; }
31
+
32
+ .topbar-right { display: flex; align-items: center; gap: 10px; }
33
+
34
+ .chip {
35
+ display: flex; align-items: center; gap: 7px;
36
+ padding: 5px 12px;
37
+ background: #141414; border: 1px solid rgba(255,255,255,0.07);
38
+ border-radius: 3px;
39
+ font-family: 'DM Mono', monospace; font-size: 11px;
40
+ color: rgba(255,255,255,0.45);
41
+ }
42
+ .chip strong { color: rgba(255,255,255,0.85); font-weight: 500; }
43
+
44
+ .btn-live {
45
+ padding: 8px 20px; border: none; border-radius: 3px;
46
+ font-family: 'Bebas Neue', sans-serif; font-size: 17px; letter-spacing: 2px;
47
+ color: #fff; cursor: pointer; transition: all 0.2s;
48
+ background: #ff2020; box-shadow: 0 0 16px rgba(255,32,32,0.25);
49
+ }
50
+ .btn-live:hover { box-shadow: 0 0 28px rgba(255,32,32,0.45); }
51
+ .btn-live.on { background: #141414; color: #ff2020; border: 1px solid rgba(255,32,32,0.3); box-shadow: none; }
52
+
53
+ .preview {
54
+ flex: 1; position: relative;
55
+ background: #000;
56
+ display: flex; align-items: center; justify-content: center;
57
+ overflow: hidden;
58
+ }
59
+
60
+ #dashVideo { max-width: 100%; max-height: 100%; display: block; object-fit: contain; background: #000; }
61
+
62
+ .preview-idle {
63
+ position: absolute; inset: 0;
64
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
65
+ pointer-events: none;
66
+ }
67
+ .preview-idle h3 { font-family: 'Bebas Neue', sans-serif; font-size: 24px; letter-spacing: 4px; color: rgba(255,255,255,0.1); }
68
+ .preview-idle p { font-family: 'DM Mono', monospace; font-size: 10px; color: rgba(255,255,255,0.12); letter-spacing: 1.5px; }
69
+
70
+ .controls {
71
+ height: 62px; padding: 0 20px; flex-shrink: 0;
72
+ background: #080808; border-top: 1px solid rgba(255,255,255,0.05);
73
+ display: flex; align-items: center; gap: 10px;
74
+ }
75
+
76
+ .ctrl {
77
+ display: flex; align-items: center; gap: 8px;
78
+ padding: 8px 16px;
79
+ background: #141414; border: 1px solid rgba(255,255,255,0.07);
80
+ border-radius: 4px;
81
+ font-size: 13px; font-weight: 500; color: rgba(255,255,255,0.45);
82
+ cursor: pointer; transition: all 0.15s; user-select: none;
83
+ }
84
+ .ctrl:hover { border-color: rgba(255,255,255,0.15); color: rgba(255,255,255,0.85); }
85
+ .ctrl.on { background: rgba(255,255,255,0.05); border-color: rgba(255,255,255,0.18); color: #fff; }
86
+
87
+ .vol-group {
88
+ display: flex; align-items: center; gap: 9px;
89
+ margin-left: auto;
90
+ font-family: 'DM Mono', monospace; font-size: 10px;
91
+ color: rgba(255,255,255,0.35); letter-spacing: 1px;
92
+ }
93
+ input[type=range] {
94
+ width: 72px; appearance: none;
95
+ height: 2px; background: rgba(255,255,255,0.15); border-radius: 2px; outline: none;
96
+ }
97
+ input[type=range]::-webkit-slider-thumb {
98
+ appearance: none; width: 11px; height: 11px;
99
+ border-radius: 50%; background: #fff; cursor: pointer;
100
+ }
101
+
102
+ .toast {
103
+ position: fixed; bottom: 18px; left: 50%;
104
+ transform: translateX(-50%) translateY(12px);
105
+ background: #181818; border: 1px solid rgba(255,255,255,0.12);
106
+ border-radius: 5px; padding: 8px 16px;
107
+ font-family: 'DM Mono', monospace; font-size: 11px; color: rgba(255,255,255,0.85);
108
+ z-index: 800; opacity: 0; transition: all 0.22s; pointer-events: none; white-space: nowrap;
109
+ }
110
+ .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
111
+ </style>
112
+ </head>
113
+ <body>
114
+ <div class="app">
115
+ <div class="topbar">
116
+ <div class="brand">STREAM</div>
117
+ <div class="topbar-right">
118
+ <div class="chip">👁 <strong id="dViewers">0</strong> viewers</div>
119
+ <div class="chip" id="dTimerChip" style="display:none">🔴 <strong id="dTimer">00:00</strong></div>
120
+ <button class="btn-live" id="btnLive" onclick="toggleLive()">GO LIVE</button>
121
+ </div>
122
+ </div>
123
+
124
+ <div class="preview">
125
+ <video id="dashVideo" autoplay playsinline muted></video>
126
+ <div class="preview-idle" id="previewIdle">
127
+ <h3>PREVIEW INACTIV</h3>
128
+ <p>ACTIVEAZĂ MICROFONUL SAU ECRANUL</p>
129
+ </div>
130
+ </div>
131
+
132
+ <div class="controls">
133
+ <div class="ctrl" id="ctrlMic" onclick="toggleMic()">🎙 Microfon</div>
134
+ <div class="ctrl" id="ctrlScreen" onclick="toggleScreen()">🖥 Ecran</div>
135
+ <div class="ctrl" id="ctrlCam" onclick="toggleCam()">📷 Cameră</div>
136
+ <div class="vol-group">
137
+ VOL
138
+ <input type="range" id="volRange" min="0" max="100" value="80" oninput="document.getElementById('volVal').textContent=this.value">
139
+ <span id="volVal">80</span>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ <div class="toast" id="toast"></div>
144
+
145
+ <script>
146
+ const WS = (() => { const p = location.protocol === 'https:' ? 'wss' : 'ws'; return `${p}://${location.host}`; })();
147
+ const ICE = [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }];
148
+
149
+ let ws, isLive = false;
150
+ let micStream = null, screenStream = null, camStream = null, combinedStream = null;
151
+ let peerConns = {}, timerIv, timerSec = 0;
152
+
153
+ function connect() {
154
+ ws = new WebSocket(WS);
155
+ ws.onopen = () => ws.send(JSON.stringify({ type: 'streamer_join' }));
156
+ ws.onmessage = e => handle(JSON.parse(e.data));
157
+ ws.onclose = () => setTimeout(connect, 3000);
158
+ }
159
+
160
+ function handle(msg) {
161
+ if (msg.type === 'streamer_welcome') document.getElementById('dViewers').textContent = msg.viewerCount;
162
+ if (msg.type === 'viewer_count') document.getElementById('dViewers').textContent = msg.count;
163
+ if (msg.type === 'new_viewer' && isLive && combinedStream) createOffer(msg.vid);
164
+ if (msg.type === 'answer' && peerConns[msg.vid]) peerConns[msg.vid].setRemoteDescription(new RTCSessionDescription(msg.sdp));
165
+ if (msg.type === 'candidate' && peerConns[msg.vid]) peerConns[msg.vid].addIceCandidate(new RTCIceCandidate(msg.candidate));
166
+ }
167
+
168
+ function toggleLive() {
169
+ if (!isLive) {
170
+ if (!combinedStream) { toast('Activează mai întâi microfonul sau ecranul!'); return; }
171
+ isLive = true;
172
+ ws.send(JSON.stringify({ type: 'go_live' }));
173
+ const btn = document.getElementById('btnLive');
174
+ btn.textContent = '⏹ STOP'; btn.classList.add('on');
175
+ document.getElementById('dTimerChip').style.display = 'flex';
176
+ timerSec = 0; clearInterval(timerIv);
177
+ timerIv = setInterval(() => {
178
+ timerSec++;
179
+ const m = String(Math.floor(timerSec/60)).padStart(2,'0');
180
+ const s = String(timerSec%60).padStart(2,'0');
181
+ document.getElementById('dTimer').textContent = `${m}:${s}`;
182
+ }, 1000);
183
+ toast('🔴 Ești LIVE!');
184
+ } else {
185
+ isLive = false;
186
+ ws.send(JSON.stringify({ type: 'end_live' }));
187
+ const btn = document.getElementById('btnLive');
188
+ btn.textContent = 'GO LIVE'; btn.classList.remove('on');
189
+ document.getElementById('dTimerChip').style.display = 'none';
190
+ clearInterval(timerIv);
191
+ Object.values(peerConns).forEach(p => p.close());
192
+ peerConns = {};
193
+ toast('Stream oprit.');
194
+ }
195
+ }
196
+
197
+ async function toggleMic() {
198
+ const c = document.getElementById('ctrlMic');
199
+ if (!micStream) {
200
+ try { micStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); c.classList.add('on'); c.textContent = '🎙 Mic ON'; rebuild(); }
201
+ catch { toast('Nu s-a putut accesa microfonul.'); }
202
+ } else { micStream.getTracks().forEach(t => t.stop()); micStream = null; c.classList.remove('on'); c.textContent = '🎙 Microfon'; rebuild(); }
203
+ }
204
+
205
+ async function toggleScreen() {
206
+ const c = document.getElementById('ctrlScreen');
207
+ if (!screenStream) {
208
+ try {
209
+ screenStream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: { ideal: 30, max: 60 }, width: { ideal: 1920 } }, audio: true });
210
+ c.classList.add('on'); c.textContent = '🖥 Ecran ON';
211
+ screenStream.getVideoTracks()[0].onended = () => { screenStream = null; c.classList.remove('on'); c.textContent = '🖥 Ecran'; rebuild(); };
212
+ rebuild();
213
+ } catch { toast('Partajare anulată.'); }
214
+ } else { screenStream.getTracks().forEach(t => t.stop()); screenStream = null; c.classList.remove('on'); c.textContent = '🖥 Ecran'; rebuild(); }
215
+ }
216
+
217
+ async function toggleCam() {
218
+ const c = document.getElementById('ctrlCam');
219
+ if (!camStream) {
220
+ try { camStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); c.classList.add('on'); c.textContent = '📷 Cam ON'; rebuild(); }
221
+ catch { toast('Nu s-a putut accesa camera.'); }
222
+ } else { camStream.getTracks().forEach(t => t.stop()); camStream = null; c.classList.remove('on'); c.textContent = '📷 Cameră'; rebuild(); }
223
+ }
224
+
225
+ function rebuild() {
226
+ const tracks = [];
227
+ if (screenStream) screenStream.getTracks().forEach(t => tracks.push(t));
228
+ else if (camStream) camStream.getTracks().forEach(t => tracks.push(t));
229
+ if (micStream) micStream.getTracks().forEach(t => tracks.push(t));
230
+ combinedStream = tracks.length ? new MediaStream(tracks) : null;
231
+ const vid = document.getElementById('dashVideo');
232
+ const idle = document.getElementById('previewIdle');
233
+ if (combinedStream) {
234
+ vid.srcObject = combinedStream; idle.style.display = 'none';
235
+ const vt = combinedStream.getVideoTracks()[0];
236
+ if (vt) { const s = vt.getSettings(); vid.style.aspectRatio = s.height > s.width ? '9/16' : '16/9'; }
237
+ if (isLive) { Object.keys(peerConns).forEach(vid => { peerConns[vid].close(); delete peerConns[vid]; createOffer(vid); }); }
238
+ } else { vid.srcObject = null; idle.style.display = 'flex'; }
239
+ }
240
+
241
+ async function createOffer(vid) {
242
+ const pc = new RTCPeerConnection({ iceServers: ICE });
243
+ peerConns[vid] = pc;
244
+ combinedStream.getTracks().forEach(t => pc.addTrack(t, combinedStream));
245
+ pc.onicecandidate = e => { if (e.candidate) ws.send(JSON.stringify({ type: 'candidate', candidate: e.candidate, vid })); };
246
+ const offer = await pc.createOffer();
247
+ await pc.setLocalDescription(offer);
248
+ ws.send(JSON.stringify({ type: 'offer', sdp: pc.localDescription, vid }));
249
+ }
250
+
251
+ let toastTm;
252
+ function toast(msg) {
253
+ const t = document.getElementById('toast');
254
+ t.textContent = msg; t.classList.add('show');
255
+ clearTimeout(toastTm); toastTm = setTimeout(() => t.classList.remove('show'), 2600);
256
+ }
257
+
258
+ connect();
259
+ </script>
260
+ </body>
261
+ </html>
index-6.html ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ro">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>STREAM</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ html, body {
10
+ background: #000;
11
+ width: 100vw; height: 100vh;
12
+ overflow: hidden;
13
+ display: flex; align-items: center; justify-content: center;
14
+ }
15
+
16
+ #viewerVideo {
17
+ display: block;
18
+ max-width: 100vw; max-height: 100vh;
19
+ background: #000;
20
+ object-fit: contain;
21
+ }
22
+
23
+ .offline {
24
+ position: fixed; inset: 0;
25
+ display: flex; flex-direction: column;
26
+ align-items: center; justify-content: center;
27
+ gap: 16px; text-align: center;
28
+ background: #000;
29
+ }
30
+
31
+ .offline-ring {
32
+ width: 64px; height: 64px;
33
+ border-radius: 50%;
34
+ border: 1px solid rgba(255,255,255,0.08);
35
+ display: flex; align-items: center; justify-content: center;
36
+ position: relative;
37
+ }
38
+ .offline-ring::before {
39
+ content: '';
40
+ position: absolute; inset: -10px; border-radius: 50%;
41
+ border: 1px solid rgba(255,255,255,0.04);
42
+ animation: rip 3s ease infinite;
43
+ }
44
+ @keyframes rip { 0%,100%{transform:scale(1);opacity:.5} 50%{transform:scale(1.12);opacity:.1} }
45
+
46
+ .offline h2 {
47
+ font-family: 'Helvetica Neue', sans-serif;
48
+ font-size: 13px; font-weight: 400;
49
+ letter-spacing: 5px;
50
+ color: rgba(255,255,255,0.2);
51
+ text-transform: uppercase;
52
+ }
53
+
54
+ .badge-live {
55
+ position: fixed; top: 18px; left: 18px;
56
+ display: none; align-items: center; gap: 7px;
57
+ padding: 5px 13px;
58
+ background: rgba(0,0,0,0.7);
59
+ backdrop-filter: blur(10px);
60
+ border: 1px solid rgba(255,30,30,0.35);
61
+ border-radius: 3px;
62
+ font-family: 'Helvetica Neue', sans-serif;
63
+ font-size: 10px; font-weight: 500;
64
+ color: #ff2020; letter-spacing: 3px;
65
+ }
66
+ .badge-live.on { display: flex; }
67
+ .dot { width: 6px; height: 6px; border-radius: 50%; background: #ff2020; box-shadow: 0 0 7px #ff2020; animation: blink 1s infinite; }
68
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.2} }
69
+ </style>
70
+ </head>
71
+ <body>
72
+ <video id="viewerVideo" autoplay playsinline></video>
73
+
74
+ <div class="offline" id="offline">
75
+ <div class="offline-ring">
76
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.18)" stroke-width="1.5">
77
+ <circle cx="12" cy="12" r="10"/>
78
+ <polygon points="10,8 16,12 10,16" fill="rgba(255,255,255,0.07)" stroke="none"/>
79
+ </svg>
80
+ </div>
81
+ <h2>Offline</h2>
82
+ </div>
83
+
84
+ <div class="badge-live" id="badgeLive"><div class="dot"></div>LIVE</div>
85
+
86
+ <script>
87
+ const WS = (() => { const p = location.protocol === 'https:' ? 'wss' : 'ws'; return `${p}://${location.host}`; })();
88
+ const ICE = [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }];
89
+
90
+ let ws, myVid, pc;
91
+
92
+ function connect() {
93
+ ws = new WebSocket(WS);
94
+ ws.onopen = () => ws.send(JSON.stringify({ type: 'viewer_join' }));
95
+ ws.onmessage = e => handle(JSON.parse(e.data));
96
+ ws.onclose = () => setTimeout(connect, 3000);
97
+ }
98
+
99
+ async function handle(msg) {
100
+ if (msg.type === 'viewer_welcome') {
101
+ myVid = msg.vid;
102
+ if (msg.isLive) goLive();
103
+ }
104
+ if (msg.type === 'stream_started') goLive();
105
+ if (msg.type === 'stream_ended') goOffline();
106
+ if (msg.type === 'offer') await doOffer(msg.sdp);
107
+ if (msg.type === 'candidate' && pc) pc.addIceCandidate(new RTCIceCandidate(msg.candidate));
108
+ }
109
+
110
+ function goLive() {
111
+ document.getElementById('offline').style.display = 'none';
112
+ document.getElementById('badgeLive').classList.add('on');
113
+ ws.send(JSON.stringify({ type: 'viewer_ready', vid: myVid }));
114
+ }
115
+
116
+ function goOffline() {
117
+ document.getElementById('offline').style.display = 'flex';
118
+ document.getElementById('badgeLive').classList.remove('on');
119
+ document.getElementById('viewerVideo').srcObject = null;
120
+ if (pc) { pc.close(); pc = null; }
121
+ }
122
+
123
+ async function doOffer(sdp) {
124
+ pc = new RTCPeerConnection({ iceServers: ICE });
125
+ pc.ontrack = e => {
126
+ const vid = document.getElementById('viewerVideo');
127
+ vid.srcObject = e.streams[0];
128
+ const track = e.streams[0].getVideoTracks()[0];
129
+ if (track) {
130
+ const s = track.getSettings();
131
+ if (s.height > s.width) {
132
+ vid.style.width = 'auto'; vid.style.height = '100vh';
133
+ } else {
134
+ vid.style.width = '100vw'; vid.style.height = 'auto';
135
+ }
136
+ }
137
+ };
138
+ pc.onicecandidate = e => {
139
+ if (e.candidate) ws.send(JSON.stringify({ type: 'candidate', candidate: e.candidate }));
140
+ };
141
+ await pc.setRemoteDescription(new RTCSessionDescription(sdp));
142
+ const ans = await pc.createAnswer();
143
+ await pc.setLocalDescription(ans);
144
+ ws.send(JSON.stringify({ type: 'answer', sdp: pc.localDescription }));
145
+ }
146
+
147
+ connect();
148
+ </script>
149
+ </body>
150
+ </html>
server.js CHANGED
@@ -4,10 +4,11 @@ const path = require("path");
4
  const { WebSocketServer } = require("ws");
5
 
6
  const PORT = process.env.PORT || 7860;
7
- const STREAMER_PASS = "122012";
8
 
9
  const httpServer = http.createServer((req, res) => {
10
- const filePath = path.join(__dirname, "index.html");
 
 
11
  fs.readFile(filePath, (err, data) => {
12
  if (err) { res.writeHead(404); return res.end("Not found"); }
13
  res.writeHead(200, { "Content-Type": "text/html" });
@@ -19,19 +20,12 @@ const wss = new WebSocketServer({ server: httpServer });
19
 
20
  let streamerWs = null;
21
  let isLive = false;
22
- let viewerCount = 0;
23
- let totalLikes = 0;
24
  const viewers = new Set();
25
 
26
  function sendTo(ws, obj) {
27
  if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj));
28
  }
29
 
30
- function broadcast(obj, exclude = null) {
31
- const msg = JSON.stringify(obj);
32
- wss.clients.forEach(c => { if (c !== exclude && c.readyState === 1) c.send(msg); });
33
- }
34
-
35
  wss.on("connection", (ws) => {
36
  ws.role = null;
37
  ws._vid = null;
@@ -40,32 +34,30 @@ wss.on("connection", (ws) => {
40
  let msg; try { msg = JSON.parse(raw); } catch { return; }
41
 
42
  switch (msg.type) {
43
- case "streamer_auth":
44
- if (msg.password === STREAMER_PASS) {
45
- ws.role = "streamer"; streamerWs = ws;
46
- sendTo(ws, { type: "auth_ok", viewerCount, totalLikes });
47
- } else sendTo(ws, { type: "auth_fail" });
48
  break;
49
 
50
  case "viewer_join":
51
  ws.role = "viewer"; ws._vid = Math.random().toString(36).slice(2);
52
- viewers.add(ws); viewerCount = viewers.size;
53
- sendTo(ws, { type: "viewer_welcome", isLive, totalLikes, vid: ws._vid });
54
- sendTo(streamerWs, { type: "viewer_count", count: viewerCount });
55
  if (isLive) sendTo(streamerWs, { type: "new_viewer", vid: ws._vid });
56
  break;
57
 
58
  case "go_live":
59
  if (ws.role !== "streamer") return;
60
  isLive = true;
61
- broadcast({ type: "stream_started" }, ws);
62
  viewers.forEach(v => sendTo(streamerWs, { type: "new_viewer", vid: v._vid }));
63
  break;
64
 
65
  case "end_live":
66
  if (ws.role !== "streamer") return;
67
  isLive = false;
68
- broadcast({ type: "stream_ended" }, ws);
69
  break;
70
 
71
  case "offer":
@@ -85,15 +77,9 @@ wss.on("connection", (ws) => {
85
  }
86
  break;
87
 
88
- case "chat":
89
- const text = (msg.text || "").slice(0, 200).trim(); if (!text) return;
90
- const name = ws.role === "streamer" ? "🎙 Streamer" : (msg.name || "Anonim").slice(0, 20);
91
- broadcast({ type: "chat", name, text, ts: Date.now(), isStreamer: ws.role === "streamer" });
92
- break;
93
-
94
- case "like":
95
- totalLikes++;
96
- broadcast({ type: "like_burst", total: totalLikes });
97
  break;
98
  }
99
  });
@@ -101,12 +87,12 @@ wss.on("connection", (ws) => {
101
  ws.on("close", () => {
102
  if (ws.role === "streamer") {
103
  streamerWs = null; isLive = false;
104
- broadcast({ type: "stream_ended" });
105
  } else if (ws.role === "viewer") {
106
- viewers.delete(ws); viewerCount = viewers.size;
107
- sendTo(streamerWs, { type: "viewer_count", count: viewerCount });
108
  }
109
  });
110
  });
111
 
112
- httpServer.listen(PORT, () => console.log(`🔴 Live platform running on port ${PORT}`));
 
4
  const { WebSocketServer } = require("ws");
5
 
6
  const PORT = process.env.PORT || 7860;
 
7
 
8
  const httpServer = http.createServer((req, res) => {
9
+ let file = "index.html";
10
+ if (req.url === "/admin.html") file = "admin.html";
11
+ const filePath = path.join(__dirname, file);
12
  fs.readFile(filePath, (err, data) => {
13
  if (err) { res.writeHead(404); return res.end("Not found"); }
14
  res.writeHead(200, { "Content-Type": "text/html" });
 
20
 
21
  let streamerWs = null;
22
  let isLive = false;
 
 
23
  const viewers = new Set();
24
 
25
  function sendTo(ws, obj) {
26
  if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj));
27
  }
28
 
 
 
 
 
 
29
  wss.on("connection", (ws) => {
30
  ws.role = null;
31
  ws._vid = null;
 
34
  let msg; try { msg = JSON.parse(raw); } catch { return; }
35
 
36
  switch (msg.type) {
37
+ case "streamer_join":
38
+ ws.role = "streamer"; streamerWs = ws;
39
+ sendTo(ws, { type: "streamer_welcome", viewerCount: viewers.size });
 
 
40
  break;
41
 
42
  case "viewer_join":
43
  ws.role = "viewer"; ws._vid = Math.random().toString(36).slice(2);
44
+ viewers.add(ws);
45
+ sendTo(ws, { type: "viewer_welcome", isLive, vid: ws._vid });
46
+ sendTo(streamerWs, { type: "viewer_count", count: viewers.size });
47
  if (isLive) sendTo(streamerWs, { type: "new_viewer", vid: ws._vid });
48
  break;
49
 
50
  case "go_live":
51
  if (ws.role !== "streamer") return;
52
  isLive = true;
53
+ viewers.forEach(v => { sendTo(v, { type: "stream_started" }); });
54
  viewers.forEach(v => sendTo(streamerWs, { type: "new_viewer", vid: v._vid }));
55
  break;
56
 
57
  case "end_live":
58
  if (ws.role !== "streamer") return;
59
  isLive = false;
60
+ viewers.forEach(v => sendTo(v, { type: "stream_ended" }));
61
  break;
62
 
63
  case "offer":
 
77
  }
78
  break;
79
 
80
+ case "viewer_ready":
81
+ ws._vid = msg.vid || ws._vid;
82
+ sendTo(streamerWs, { type: "new_viewer", vid: ws._vid });
 
 
 
 
 
 
83
  break;
84
  }
85
  });
 
87
  ws.on("close", () => {
88
  if (ws.role === "streamer") {
89
  streamerWs = null; isLive = false;
90
+ viewers.forEach(v => sendTo(v, { type: "stream_ended" }));
91
  } else if (ws.role === "viewer") {
92
+ viewers.delete(ws);
93
+ sendTo(streamerWs, { type: "viewer_count", count: viewers.size });
94
  }
95
  });
96
  });
97
 
98
+ httpServer.listen(PORT, () => console.log(`Running on port ${PORT}`));