require('dotenv').config(); const express = require('express'); const cors = require('cors'); const NodeMediaServer = require('node-media-server'); const childProcess = require('child_process'); const fs = require('fs'); const path = require('path'); const roomsRouter = require('./routes/rooms'); const srsRouter = require('./routes/srs'); const friendsRouter = require('./routes/friends'); const viewHistoryRouter = require('./routes/viewHistory'); const errorHandler = require('./middleware/errorHandler'); const roomStore = require('./store/roomStore'); const app = express(); const PORT = process.env.PORT || 3001; const startMediaServer = () => { const host = process.env.SRS_HOST || 'localhost'; const rtmpPort = Number(process.env.SRS_RTMP_PORT || 1935); const httpPort = Number(process.env.SRS_HTTP_PORT || 8080); const rawFfmpegPath = process.env.FFMPEG_PATH; const embeddedEnabledRaw = process.env.EMBEDDED_MEDIA_SERVER; const embeddedEnabled = embeddedEnabledRaw == null ? true : !['0', 'false', 'off', 'no'].includes(String(embeddedEnabledRaw).toLowerCase()); if (!embeddedEnabled) { console.log('[Media Server] Embedded NodeMediaServer disabled (EMBEDDED_MEDIA_SERVER=0).'); return; } let ffmpegPath = rawFfmpegPath; if (!ffmpegPath) { try { if (process.platform === 'win32') { const out = childProcess.execSync('where ffmpeg', { stdio: ['ignore', 'pipe', 'ignore'] }); const first = String(out || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0]; if (first) ffmpegPath = first; } else { const out = childProcess.execSync('which ffmpeg', { stdio: ['ignore', 'pipe', 'ignore'] }); const first = String(out || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0]; if (first) ffmpegPath = first; } } catch (_) { ffmpegPath = null; } } if (ffmpegPath) { const p = String(ffmpegPath).trim().replace(/^"|"$/g, ''); ffmpegPath = p; try { if (fs.existsSync(ffmpegPath) && fs.statSync(ffmpegPath).isDirectory()) { ffmpegPath = path.join(ffmpegPath, 'ffmpeg.exe'); } } catch (_) { ffmpegPath = p; } if (!fs.existsSync(ffmpegPath)) { console.warn(`[Media Server] FFMPEG_PATH set but not found: ${ffmpegPath}`); ffmpegPath = null; } } try { const nmsConfig = { rtmp: { port: rtmpPort, chunk_size: 60000, gop_cache: true, ping: 30, ping_timeout: 60 }, http: { port: httpPort, mediaroot: './media', allow_origin: '*' } }; if (ffmpegPath) { nmsConfig.trans = { ffmpeg: ffmpegPath, tasks: [ { app: 'live', hls: true, hlsFlags: '[hls_time=2:hls_list_size=6:hls_flags=delete_segments]' } ] }; } const nms = new NodeMediaServer(nmsConfig); nms.on('prePublish', (id, streamPath) => { const parts = String(streamPath || '').split('/').filter(Boolean); const streamKey = parts[1]; if (streamKey) roomStore.setLiveStatus(streamKey, true); }); nms.on('donePublish', (id, streamPath) => { const parts = String(streamPath || '').split('/').filter(Boolean); const streamKey = parts[1]; console.log(`[Media Server] 推流结束: streamKey=${streamKey}`); // 暂时禁用自动关闭直播,改为手动控制 // if (streamKey) roomStore.setLiveStatus(streamKey, false); }); nms.run(); console.log(`Media Server RTMP: rtmp://${host}:${rtmpPort}/live (Stream Key = streamKey)`); console.log(`Media Server HTTP-FLV: http://${host}:${httpPort}/live/{streamKey}.flv`); if (ffmpegPath) { console.log(`Media Server HLS: http://${host}:${httpPort}/live/{streamKey}/index.m3u8`); console.log(`[Media Server] Using FFmpeg: ${ffmpegPath}`); } else { console.log('Media Server HLS disabled (set FFMPEG_PATH to enable HLS)'); } } catch (e) { console.error('[Media Server] Failed to start embedded media server:', e.message); console.error('[Media Server] If ports 1935/8080 are in use, stop the occupying process or change SRS_RTMP_PORT/SRS_HTTP_PORT.'); } }; startMediaServer(); // 中间件 app.use(cors({ origin: process.env.CLIENT_URL || 'http://localhost:3000', credentials: true })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // 路由 app.use('/api/rooms', roomsRouter); app.use('/api/srs', srsRouter); app.use('/api/front', friendsRouter); app.use('/api/front/activity', viewHistoryRouter); // 健康检查 app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // 错误处理 app.use(errorHandler); // 启动服务 app.listen(PORT, '0.0.0.0', () => { console.log(`API 服务运行在 http://localhost:${PORT}`); console.log(`API 服务也可通过 http://0.0.0.0:${PORT} 访问(用于 Android 模拟器)`); console.log(`SRS RTMP: rtmp://${process.env.SRS_HOST || 'localhost'}:${process.env.SRS_RTMP_PORT || 1935}/live/{streamKey}`); console.log(`SRS HTTP: http://${process.env.SRS_HOST || 'localhost'}:${process.env.SRS_HTTP_PORT || 8080}/live/{streamKey}.flv`); }); module.exports = app;