250 lines
8.8 KiB
JavaScript
250 lines
8.8 KiB
JavaScript
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(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>VideoReader Global Server</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; margin: 40px; }
|
||
.status { background: #f0f0f0; padding: 20px; border-radius: 5px; }
|
||
.channel { margin: 10px 0; padding: 10px; background: #e8f4fd; border-radius: 3px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>🌐 VideoReader Global Server</h1>
|
||
<div class="status">
|
||
<h2>Server Status</h2>
|
||
<p><strong>Status:</strong> Running</p>
|
||
<p><strong>Signaling Port:</strong> ${PORT}</p>
|
||
<p><strong>Media Port:</strong> ${MEDIA_PORT}</p>
|
||
<p><strong>Active Channels:</strong> <span id="channelCount">Loading...</span></p>
|
||
</div>
|
||
|
||
<h2>📡 Configuration</h2>
|
||
<pre>
|
||
{
|
||
"SignalingServer": "${req.get('host')}",
|
||
"DataPort": ${MEDIA_PORT},
|
||
"DefaultChannel": 10,
|
||
"FallbackIP": "127.0.0.1",
|
||
"UseSSL": false,
|
||
"ProfileName": "local"
|
||
}
|
||
</pre>
|
||
|
||
<h2>📊 Active Channels</h2>
|
||
<div id="channels">Loading...</div>
|
||
|
||
<script>
|
||
async function updateStatus() {
|
||
try {
|
||
const response = await fetch('/api/channels');
|
||
const channels = await response.json();
|
||
|
||
document.getElementById('channelCount').textContent = channels.length;
|
||
|
||
const channelsDiv = document.getElementById('channels');
|
||
channelsDiv.innerHTML = channels.length === 0
|
||
? '<p>No active channels</p>'
|
||
: channels.map(ch =>
|
||
\`<div class="channel">
|
||
<strong>Channel \${ch.channel}:</strong>
|
||
\${ch.connections} connections
|
||
<small>(created: \${new Date(ch.created).toLocaleString()})</small>
|
||
</div>\`
|
||
).join('');
|
||
} catch (e) {
|
||
console.error('Failed to update status:', e);
|
||
}
|
||
}
|
||
|
||
updateStatus();
|
||
setInterval(updateStatus, 5000);
|
||
</script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
});
|
||
|
||
// 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 }; |