init commit
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user