160 lines
5.1 KiB
JavaScript
160 lines
5.1 KiB
JavaScript
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 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.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;
|