/** * AeThex LIVE - Main Server * Next.js + Socket.io + YouTube Live Detector + RTMP ingest * * Port 3000: HTTP (Next.js + Socket.io) * Port 1935: RTMP ingest (node-media-server) * Port 8000: HLS output (node-media-server) */ const { createServer } = require('http'); const { Server } = require('socket.io'); const next = require('next'); const NodeMediaServer = require('node-media-server'); const path = require('path'); const fs = require('fs'); const https = require('https'); const dev = process.env.NODE_ENV !== 'production'; const app = next({ dev }); const handle = app.getRequestHandler(); // ── YOUTUBE LIVE DETECTOR ────────────────────────────────────────────────── const YOUTUBE_CHANNEL_ID = 'UC22sQjrETb_YaTJqh36sxLQ'; const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY || null; const POLL_INTERVAL = 60000; // 60 seconds let currentScene = 'offline'; let liveVideoId = null; let io = null; function checkYouTubeLive() { if (YOUTUBE_API_KEY) { const url = `https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${YOUTUBE_CHANNEL_ID}&type=video&eventType=live&key=${YOUTUBE_API_KEY}`; https.get(url, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const json = JSON.parse(data); const wasLive = currentScene === 'live'; if (json.items && json.items.length > 0) { liveVideoId = json.items[0].id.videoId; if (!wasLive) { console.log(`[DETECTOR] MrPiglr\u00a0is LIVE: ${liveVideoId}`); setScene('live'); } } else { liveVideoId = null; if (wasLive) { console.log('[DETECTOR] MrPiglr went offline'); setScene('offline'); } } } catch (e) { console.error('[DETECTOR] Parse error:', e.message); } }); }).on('error', err => console.error('[DETECTOR] Request error:', err.message)); } else { const options = { hostname: 'www.youtube.com', path: `/channel/${YOUTUBE_CHANNEL_ID}/live`, headers: { 'User-Agent': 'Mozilla/5.0' } }; https.get(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { const wasLive = currentScene === 'live'; const isLive = data.includes('"isLiveNow":true') || data.includes('"liveBroadcastContent":"live"'); const vidMatch = data.match(/"videoId":"([^"]+)"/); if (isLive) { if (vidMatch) liveVideoId = vidMatch[1]; if (!wasLive) { console.log('[DETECTOR] MrPiglr is LIVE (scraped)'); setScene('live'); } } else { liveVideoId = null; if (wasLive) { console.log('[DETECTOR] MrPiglr went offline (scraped)'); setScene('offline'); } } }); }).on('error', err => console.error('[DETECTOR] Scrape error:', err.message)); } } function setScene(scene, triggeredBy = 'system') { const prev = currentScene; currentScene = scene; console.log(`[SCENE] ${prev} => ${scene} (by: ${triggeredBy})`); if (io) io.emit('scene:change', { scene, liveVideoId, triggeredBy, timestamp: Date.now() }); } function processCommand(msg, socket, username) { if (!msg.startsWith('/')) return false; const parts = msg.trim().split(' '); const cmd = parts[0].toLowerCase(); const args = parts.slice(1).join(' '); const commands = { '/music': () => setScene('music', username), '/kael': () => { setScene('kael', username); return { kaelQuery: args }; }, '/passport': () => { setScene('passport', username); return { passportUser: args }; }, '/oakdale': () => setScene('oakdale', username), '/forge': () => setScene('forge', username), '/live': () => setScene('live', username), '/offline': () => setScene('offline', username), '/help': () => null, }; if (commands[cmd]) { commands[cmd](); if (cmd === '/kael' && args) io.emit('kael:query', { query: args, username }); if (cmd === '/passport' && args) io.emit('passport:lookup', { username: args, requestedBy: username }); io.emit('chat:message', { id: Date.now().toString(), username, message: msg, type: 'command', timestamp: Date.now() }); return true; } return false; } const mediaDir = path.join(__dirname, 'media'); if (!fs.existsSync(mediaDir)) fs.mkdirSync(mediaDir, { recursive: true }); const nms = new NodeMediaServer({ rtmp: { port: 1935, chunk_size: 60000, gop_cache: true, ping: 30, ping_timeout: 60 }, http: { port: 8000, mediaroot: mediaDir, allow_origin: '*', api: true }, }); nms.on('prePublish', (id, StreamPath) => { console.log(`[RTMP] Stream started: ${StreamPath}`); setScene('live', 'obs-push'); if (io) io.emit('rtmp:started', { streamPath: StreamPath }); }); nms.on('donePublish', (id, StreamPath) => { console.log(`[RTMP] Stream ended: ${StreamPath}`); setScene('offline', 'obs-end'); if (io) io.emit('rtmp:ended', {}); }); app.prepare().then(() => { let viewerCount = 0; const httpServer = createServer((req, res) => { if (req.method === 'POST' && req.url === '/_internal/scene') { if (req.headers['x-internal'] !== 'true') { res.writeHead(403); res.end(); return; } let body = ''; req.on('data', c => body += c); req.on('end', () => { try { const { scene } = JSON.parse(body); setScene(scene, 'api'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, scene })); } catch { res.writeHead(400); res.end(); } }); return; } if (req.method === 'GET' && req.url === '/_internal/status') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ scene: currentScene, liveVideoId, viewers: viewerCount, uptime: process.uptime() })); return; } handle(req, res); }); io = new Server(httpServer, { cors: { origin: '*', methods: ['GET', 'POST'] } }); io.on('connection', (socket) => { viewerCount++; io.emit('viewers:update', viewerCount); socket.emit('state:init', { scene: currentScene, liveVideoId, timestamp: Date.now() }); socket.on('chat:send', ({ message, username }) => { if (!message || !username) return; if (!processCommand(message, socket, username)) { io.emit('chat:message', { id: Date.now().toString() + Math.random(), username, message, type: 'chat', timestamp: Date.now() }); } }); socket.on('scene:request', ({ scene, username }) => setScene(scene, username || 'viewer')); socket.on('kael:response', (data) => io.emit('kael:response', data)); socket.on('disconnect', () => { viewerCount = Math.max(0, viewerCount - 1); io.emit('viewers:update', viewerCount); }); }); nms.run(); checkYouTubeLive(); setInterval(checkYouTubeLive, POLL_INTERVAL); const PORT = process.env.PORT || 3000; httpServer.listen(PORT, () => { console.log('\n' + '═'.repeat(60)); console.log(' 🔴 AeThex LIVE - Broadcast Network'); console.log('═'.repeat(60)); console.log(` HTTP: http://localhost:${PORT}`); console.log(` RTMP: rtmp://localhost:1935/live`); console.log(` Scene: ${currentScene}`); console.log('═'.repeat(60) + '\n'); }); });