zhibo/live-streaming/server/index.js

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;