const express = require('express');
const net = require('net');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
const MEDIA_PORT = process.env.MEDIA_PORT || 5000;
// Логирование
const log = (message) => {
console.log(`[${new Date().toISOString()}] ${message}`);
};
// Хранилище активных каналов
const activeChannels = new Map();
// Middleware
app.use(express.json());
app.use((req, res, next) => {
log(`${req.method} ${req.url} from ${req.ip}`);
next();
});
// CORS для всех доменов (в продакшене настроить конкретные домены)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
// Маршрут для получения IP медиа-сервера (совместимость с оригинальным API)
app.get('/get-ip-kr.php', (req, res) => {
const port = req.query.port;
log(`IP request for channel: ${port}`);
// В простой реализации возвращаем IP этого же сервера
// В продакшене здесь может быть логика балансировки
const mediaServerIP = process.env.MEDIA_SERVER_IP || '127.0.0.1';
// Возвращаем IP в бинарном формате (4 байта) как в оригинале
const ipParts = mediaServerIP.split('.').map(n => parseInt(n));
const buffer = Buffer.from(ipParts);
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Length': buffer.length
});
res.end(buffer);
log(`Returned IP: ${mediaServerIP} for channel ${port}`);
});
// REST API для управления
app.get('/api/status', (req, res) => {
res.json({
status: 'running',
activeChannels: activeChannels.size,
uptime: process.uptime(),
channels: Array.from(activeChannels.keys())
});
});
app.get('/api/channels', (req, res) => {
const channelList = Array.from(activeChannels.entries()).map(([channel, data]) => ({
channel,
connections: data.connections,
created: data.created
}));
res.json(channelList);
});
// Веб-интерфейс
app.get('/', (req, res) => {
res.send(`
VideoReader Global Server
🌐 VideoReader Global Server
Server Status
Status: Running
Signaling Port: ${PORT}
Media Port: ${MEDIA_PORT}
Active Channels: Loading...
📡 Configuration
{
"SignalingServer": "${req.get('host')}",
"DataPort": ${MEDIA_PORT},
"DefaultChannel": 10,
"FallbackIP": "127.0.0.1",
"UseSSL": false,
"ProfileName": "local"
}
📊 Active Channels
Loading...
`);
});
// TCP медиа-сервер
const mediaServer = net.createServer((socket) => {
let channel = null;
let deviceType = null;
log(`New connection from ${socket.remoteAddress}:${socket.remotePort}`);
socket.on('data', (data) => {
if (channel === null && data.length >= 2) {
// Первые 2 байта: тип устройства (0=receiver, 1=sender) и канал
deviceType = data[0];
channel = data[1];
log(`Device connected - Type: ${deviceType === 0 ? 'receiver' : 'sender'}, Channel: ${channel}`);
// Регистрируем канал
if (!activeChannels.has(channel)) {
activeChannels.set(channel, {
connections: 0,
created: new Date().toISOString(),
receivers: [],
senders: []
});
}
const channelData = activeChannels.get(channel);
channelData.connections++;
if (deviceType === 0) {
channelData.receivers.push(socket);
} else {
channelData.senders.push(socket);
}
// Обработка данных после установки канала
if (data.length > 2) {
handleMediaData(socket, data.slice(2), channel, deviceType);
}
} else if (channel !== null) {
// Передача медиа-данных
handleMediaData(socket, data, channel, deviceType);
}
});
socket.on('close', () => {
if (channel !== null) {
const channelData = activeChannels.get(channel);
if (channelData) {
channelData.connections--;
// Удаляем сокет из соответствующего массива
if (deviceType === 0) {
channelData.receivers = channelData.receivers.filter(s => s !== socket);
} else {
channelData.senders = channelData.senders.filter(s => s !== socket);
}
// Удаляем канал если нет подключений
if (channelData.connections <= 0) {
activeChannels.delete(channel);
log(`Channel ${channel} removed - no active connections`);
}
}
}
log(`Connection closed from ${socket.remoteAddress}:${socket.remotePort}`);
});
socket.on('error', (err) => {
log(`Socket error: ${err.message}`);
});
});
function handleMediaData(fromSocket, data, channel, fromType) {
const channelData = activeChannels.get(channel);
if (!channelData) return;
// Пересылаем данные от отправителей к получателям и наоборот
const targetSockets = fromType === 0 ? channelData.senders : channelData.receivers;
targetSockets.forEach(socket => {
if (socket !== fromSocket && socket.writable) {
try {
socket.write(data);
} catch (err) {
log(`Error forwarding data: ${err.message}`);
}
}
});
}
// Запуск серверов
app.listen(PORT, () => {
log(`🌐 Signaling server running on port ${PORT}`);
log(`📡 Web interface: http://localhost:${PORT}`);
});
mediaServer.listen(MEDIA_PORT, () => {
log(`📺 Media server running on port ${MEDIA_PORT}`);
});
// Graceful shutdown
process.on('SIGINT', () => {
log('Shutting down servers...');
mediaServer.close();
process.exit(0);
});
// Экспорт для возможности тестирования
module.exports = { app, mediaServer };