81 lines
2.6 KiB
JavaScript
81 lines
2.6 KiB
JavaScript
require('dotenv').config();
|
|
const express = require('express');
|
|
const http = require('http');
|
|
const { Server } = require("socket.io");
|
|
const axios = require('axios');
|
|
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
const io = new Server(server);
|
|
|
|
app.use(express.static('public'));
|
|
|
|
// --- Cloudflare Credentials ---
|
|
app.get('/api/get-turn-credentials', async (req, res) => {
|
|
try {
|
|
const response = await axios.post(
|
|
`https://rtc.live.cloudflare.com/v1/turn/keys/${process.env.CLOUDFLARE_APP_ID}/credentials/generate-ice-servers`,
|
|
{ ttl: 86400 },
|
|
{ headers: { 'Authorization': `Bearer ${process.env.CLOUDFLARE_API_TOKEN}`, 'Content-Type': 'application/json' } }
|
|
);
|
|
res.json(response.data);
|
|
} catch (error) {
|
|
console.error("Cloudflare error:", error.message);
|
|
res.status(500).json({ error: "Failed to fetch credentials" });
|
|
}
|
|
});
|
|
|
|
// --- Signaling Logic ---
|
|
let streamers = {}; // socketId -> { name: "Gaming Stream" }
|
|
// Add a map to track relationships: viewerSocketId -> streamerSocketId
|
|
let relationships = {};
|
|
|
|
io.on('connection', (socket) => {
|
|
// 1. Immediately send the list of active streams to the new user
|
|
socket.emit('streamer_list_update', streamers);
|
|
|
|
// 2. User wants to go live
|
|
socket.on('start_stream', (name) => {
|
|
streamers[socket.id] = { name: name };
|
|
io.emit('streamer_list_update', streamers); // Notify everyone
|
|
});
|
|
|
|
// 3. User wants to watch a specific streamer
|
|
socket.on('join_stream', (streamerId) => {
|
|
relationships[socket.id] = streamerId; // Record the link
|
|
io.to(streamerId).emit('viewer_joined', { viewerId: socket.id });
|
|
});
|
|
|
|
// 4. WebRTC Signaling (Offer/Answer/ICE)
|
|
socket.on('webrtc_offer', (data) => {
|
|
io.to(data.target).emit('webrtc_offer', { sdp: data.sdp, sender: socket.id });
|
|
});
|
|
|
|
socket.on('webrtc_answer', (data) => {
|
|
io.to(data.target).emit('webrtc_answer', { sdp: data.sdp, sender: socket.id });
|
|
});
|
|
|
|
socket.on('ice_candidate', (data) => {
|
|
io.to(data.target).emit('ice_candidate', { candidate: data.candidate, sender: socket.id });
|
|
});
|
|
|
|
// 5. Cleanup
|
|
socket.on('disconnect', () => {
|
|
// 1. If a STREAMER left (existing code)
|
|
if (streamers[socket.id]) {
|
|
delete streamers[socket.id];
|
|
io.emit('streamer_list_update', streamers);
|
|
}
|
|
|
|
// 2. If a VIEWER left (NEW CODE)
|
|
if (relationships[socket.id]) {
|
|
const streamerId = relationships[socket.id];
|
|
// Tell the specific streamer that this specific viewer is gone
|
|
io.to(streamerId).emit('viewer_left', { viewerId: socket.id });
|
|
delete relationships[socket.id];
|
|
}
|
|
});
|
|
});
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
server.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); |