aethex-live-v2/server.js
2026-03-21 08:41:53 +00:00

184 lines
7.2 KiB
JavaScript

/**
* 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');
});
});