| const http = require("http"); |
| const fs = require("fs"); |
| const path = require("path"); |
| const { WebSocketServer } = require("ws"); |
|
|
| const PORT = process.env.PORT || 7860; |
|
|
| const httpServer = http.createServer((req, res) => { |
| let file = "index.html"; |
| if (req.url === "/admin.html") file = "admin.html"; |
| const filePath = path.join(__dirname, file); |
| fs.readFile(filePath, (err, data) => { |
| if (err) { res.writeHead(404); return res.end("Not found"); } |
| res.writeHead(200, { "Content-Type": "text/html" }); |
| res.end(data); |
| }); |
| }); |
|
|
| const wss = new WebSocketServer({ server: httpServer }); |
|
|
| let streamerWs = null; |
| let isLive = false; |
| const viewers = new Set(); |
|
|
| function sendTo(ws, obj) { |
| if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); |
| } |
|
|
| wss.on("connection", (ws) => { |
| ws.role = null; |
| ws._vid = null; |
|
|
| ws.on("message", (raw) => { |
| let msg; try { msg = JSON.parse(raw); } catch { return; } |
|
|
| switch (msg.type) { |
| case "streamer_join": |
| ws.role = "streamer"; streamerWs = ws; |
| sendTo(ws, { type: "streamer_welcome", viewerCount: viewers.size }); |
| break; |
|
|
| case "viewer_join": |
| ws.role = "viewer"; ws._vid = Math.random().toString(36).slice(2); |
| viewers.add(ws); |
| sendTo(ws, { type: "viewer_welcome", isLive, vid: ws._vid }); |
| sendTo(streamerWs, { type: "viewer_count", count: viewers.size }); |
| if (isLive) sendTo(streamerWs, { type: "new_viewer", vid: ws._vid }); |
| break; |
|
|
| case "go_live": |
| if (ws.role !== "streamer") return; |
| isLive = true; |
| viewers.forEach(v => { sendTo(v, { type: "stream_started" }); }); |
| viewers.forEach(v => sendTo(streamerWs, { type: "new_viewer", vid: v._vid })); |
| break; |
|
|
| case "end_live": |
| if (ws.role !== "streamer") return; |
| isLive = false; |
| viewers.forEach(v => sendTo(v, { type: "stream_ended" })); |
| break; |
|
|
| case "offer": |
| if (ws.role !== "streamer") return; |
| viewers.forEach(v => { if (v._vid === msg.vid) sendTo(v, { type: "offer", sdp: msg.sdp }); }); |
| break; |
|
|
| case "answer": |
| sendTo(streamerWs, { type: "answer", sdp: msg.sdp, vid: ws._vid }); |
| break; |
|
|
| case "candidate": |
| if (ws.role === "streamer") { |
| viewers.forEach(v => { if (v._vid === msg.vid) sendTo(v, { type: "candidate", candidate: msg.candidate }); }); |
| } else { |
| sendTo(streamerWs, { type: "candidate", candidate: msg.candidate, vid: ws._vid }); |
| } |
| break; |
|
|
| case "viewer_ready": |
| ws._vid = msg.vid || ws._vid; |
| sendTo(streamerWs, { type: "new_viewer", vid: ws._vid }); |
| break; |
| } |
| }); |
|
|
| ws.on("close", () => { |
| if (ws.role === "streamer") { |
| streamerWs = null; isLive = false; |
| viewers.forEach(v => sendTo(v, { type: "stream_ended" })); |
| } else if (ws.role === "viewer") { |
| viewers.delete(ws); |
| sendTo(streamerWs, { type: "viewer_count", count: viewers.size }); |
| } |
| }); |
| }); |
|
|
| httpServer.listen(PORT, () => console.log(`Running on port ${PORT}`)); |
|
|