184 lines
7.2 KiB
JavaScript
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');
|
|
});
|
|
});
|