main commit
This commit is contained in:
4016
backend/god-eye.log
4016
backend/god-eye.log
File diff suppressed because it is too large
Load Diff
@@ -61,6 +61,40 @@ function initializeSocket() {
|
||||
socket.on('device:disconnected', (data) => {
|
||||
logMessage('warn', `Устройство отключено: ${data.deviceId}`);
|
||||
});
|
||||
|
||||
// Обработчики событий сессий
|
||||
socket.on('session:created', (data) => {
|
||||
logMessage('info', `Сессия создана: ${data.sessionId} для устройства ${data.deviceId}`);
|
||||
updateOperatorSessions();
|
||||
});
|
||||
|
||||
socket.on('session:accepted', (data) => {
|
||||
logMessage('info', `Сессия принята: ${data.sessionId}`);
|
||||
currentSessionId = data.sessionId;
|
||||
document.getElementById('current-session-id').value = currentSessionId;
|
||||
|
||||
// Показываем кнопку веб-камеры для оператора
|
||||
showOperatorWebcamButton();
|
||||
|
||||
updateOperatorSessions();
|
||||
showAlert('Сессия принята! Можно начинать WebRTC соединение.', 'success');
|
||||
});
|
||||
|
||||
socket.on('session:rejected', (data) => {
|
||||
logMessage('warn', `Сессия отклонена: ${data.sessionId} - ${data.error}`);
|
||||
updateOperatorSessions();
|
||||
showAlert(`Сессия отклонена: ${data.error}`, 'warning');
|
||||
});
|
||||
|
||||
socket.on('session:ended', (data) => {
|
||||
logMessage('info', `Сессия завершена: ${data.sessionId}`);
|
||||
updateOperatorSessions();
|
||||
|
||||
// Скрываем элементы веб-камеры
|
||||
hideOperatorWebcamButton();
|
||||
const webcamVideo = document.getElementById('operatorWebcam');
|
||||
if (webcamVideo) webcamVideo.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Обновление статуса подключения
|
||||
@@ -300,6 +334,14 @@ function connectOperator() {
|
||||
document.getElementById('operator-disconnect').disabled = false;
|
||||
|
||||
showAvailableDevices(data.availableDevices || []);
|
||||
updateOperatorSessions(); // Обновляем список сессий после подключения
|
||||
|
||||
// Автоматическое обновление сессий каждые 3 секунды
|
||||
setInterval(() => {
|
||||
if (operatorSocket && operatorSocket.connected) {
|
||||
updateOperatorSessions();
|
||||
}
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
operatorSocket.on('device:connected', (data) => {
|
||||
@@ -310,6 +352,12 @@ function connectOperator() {
|
||||
operatorSocket.on('camera:stream-ready', (data) => {
|
||||
logMessage('info', `Камера готова: ${data.sessionId}`);
|
||||
showOperatorSession(data.sessionId, 'active');
|
||||
updateOperatorSessions();
|
||||
});
|
||||
|
||||
operatorSocket.on('camera:request-sent', (data) => {
|
||||
logMessage('info', `Запрос отправлен: ${data.sessionId}`);
|
||||
updateOperatorSessions();
|
||||
});
|
||||
|
||||
operatorSocket.on('camera:denied', (data) => {
|
||||
@@ -497,26 +545,7 @@ async function endOperatorSession(sessionId) {
|
||||
}
|
||||
|
||||
async function refreshSessions() {
|
||||
const operatorId = document.getElementById('operator-id').value;
|
||||
if (!operatorId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/operators/sessions', {
|
||||
headers: { 'X-Operator-Id': operatorId }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const container = document.getElementById('operator-sessions');
|
||||
container.innerHTML = '';
|
||||
|
||||
data.sessions.forEach(session => {
|
||||
showOperatorSession(session.sessionId, session.status);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logMessage('error', 'Ошибка загрузки сессий: ' + error.message);
|
||||
}
|
||||
updateOperatorSessions();
|
||||
}
|
||||
|
||||
// === ADMIN FUNCTIONS ===
|
||||
@@ -780,9 +809,95 @@ function stopLocalVideo() {
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательная функция для показа уведомлений
|
||||
function showAlert(type, message) {
|
||||
// Создаем временное уведомление
|
||||
// Обновление списка сессий оператора
|
||||
function updateOperatorSessions() {
|
||||
console.log('updateOperatorSessions called, operatorSocket:', operatorSocket);
|
||||
|
||||
if (!operatorSocket) {
|
||||
console.log('No operatorSocket, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!operatorSocket.connected) {
|
||||
console.log('operatorSocket not connected, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Requesting sessions list...');
|
||||
operatorSocket.emit('sessions:list', {}, (sessions) => {
|
||||
console.log('Received sessions:', sessions);
|
||||
|
||||
const container = document.getElementById('operator-sessions');
|
||||
if (!container) {
|
||||
console.log('No operator-sessions container found');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!sessions || sessions.length === 0) {
|
||||
container.innerHTML = '<p>Нет активных сессий</p>';
|
||||
console.log('No sessions to display');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Displaying', sessions.length, 'sessions');
|
||||
sessions.forEach(session => {
|
||||
const sessionCard = document.createElement('div');
|
||||
sessionCard.className = `session-card session-${session.status}`;
|
||||
|
||||
const statusText = {
|
||||
'pending': 'Ожидание',
|
||||
'active': 'Активна',
|
||||
'rejected': 'Отклонена',
|
||||
'ended': 'Завершена'
|
||||
};
|
||||
|
||||
sessionCard.innerHTML = `
|
||||
<h4>Сессия: ${session.sessionId ? session.sessionId.substr(0, 8) : session.id ? session.id.substr(0, 8) : 'N/A'}...</h4>
|
||||
<p><strong>Устройство:</strong> ${session.deviceId}</p>
|
||||
<p><strong>Камера:</strong> ${session.cameraType}</p>
|
||||
<p><strong>Статус:</strong> ${statusText[session.status] || session.status}</p>
|
||||
<p><strong>Создана:</strong> ${new Date(session.createdAt).toLocaleString()}</p>
|
||||
${session.status === 'active' ? `
|
||||
<button class="btn" onclick="switchCamera('${session.sessionId || session.id}', 'front')">Фронтальная</button>
|
||||
<button class="btn" onclick="switchCamera('${session.sessionId || session.id}', 'back')">Основная</button>
|
||||
<button class="btn btn-danger" onclick="endSession('${session.sessionId || session.id}')">Завершить</button>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
container.appendChild(sessionCard);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Завершение сессии
|
||||
function endSession(sessionId) {
|
||||
if (!operatorSocket) {
|
||||
showAlert('Не подключен как оператор', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
operatorSocket.emit('session:end', { sessionId });
|
||||
logMessage('info', `Завершаем сессию: ${sessionId}`);
|
||||
}
|
||||
|
||||
// Переключение камеры
|
||||
function switchCamera(sessionId, cameraType) {
|
||||
if (!operatorSocket) {
|
||||
showAlert('Не подключен как оператор', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
operatorSocket.emit('camera:switch', {
|
||||
sessionId,
|
||||
cameraType
|
||||
});
|
||||
logMessage('info', `Переключаем камеру: ${sessionId} на ${cameraType}`);
|
||||
}
|
||||
|
||||
// Показ уведомления
|
||||
function showAlert(message, type) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
@@ -794,7 +909,6 @@ function showAlert(type, message) {
|
||||
|
||||
document.body.appendChild(alert);
|
||||
|
||||
// Удаляем через 5 секунд
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(alert)) {
|
||||
document.body.removeChild(alert);
|
||||
|
||||
815
backend/public/mobile.html
Normal file
815
backend/public/mobile.html
Normal file
@@ -0,0 +1,815 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>GodEye Mobile Camera</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.mobile-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 18px;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background: #4CAF50;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status.disconnected {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#localVideo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: scaleX(-1); /* Зеркало для фронтальной камеры */
|
||||
}
|
||||
|
||||
.no-camera {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
padding: 15px 20px;
|
||||
border-radius: 25px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-camera {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-camera:hover {
|
||||
background: #45a049;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.btn-camera:disabled {
|
||||
background: #666;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-switch {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-switch:hover {
|
||||
background: #1976D2;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.btn-disconnect {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-disconnect:hover {
|
||||
background: #d32f2f;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.device-info {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 15px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.requests-panel {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.requests-panel.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.request-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.btn-accept {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-reject {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logs {
|
||||
position: absolute;
|
||||
bottom: 120px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(10px);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logs.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.log-entry.info { color: #2196F3; }
|
||||
.log-entry.success { color: #4CAF50; }
|
||||
.log-entry.warning { color: #FF9800; }
|
||||
.log-entry.error { color: #f44336; }
|
||||
|
||||
@media (orientation: landscape) {
|
||||
.controls {
|
||||
right: 20px;
|
||||
left: auto;
|
||||
transform: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mobile-container">
|
||||
<div class="header">
|
||||
<h1>📱 GodEye Mobile</h1>
|
||||
<div id="status" class="status disconnected">Отключено</div>
|
||||
</div>
|
||||
|
||||
<div class="video-container">
|
||||
<video id="localVideo" autoplay playsinline muted></video>
|
||||
<div id="no-camera" class="no-camera">
|
||||
<h3>📷 Камера не активна</h3>
|
||||
<p>Нажмите кнопку включения камеры</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="device-info" id="device-info">
|
||||
<div>📱 <strong>Device ID:</strong> <span id="device-id">Генерация...</span></div>
|
||||
<div>📷 <strong>Camera:</strong> <span id="current-camera">none</span></div>
|
||||
<div>🔗 <strong>Sessions:</strong> <span id="session-count">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="btn-camera" class="btn btn-camera" title="Включить/выключить камеру">
|
||||
📷
|
||||
</button>
|
||||
<button id="btn-switch" class="btn btn-switch" title="Переключить камеру">
|
||||
🔄
|
||||
</button>
|
||||
<button id="btn-disconnect" class="btn btn-disconnect" title="Отключиться">
|
||||
❌
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="requests-panel" class="requests-panel">
|
||||
<h3>📞 Запрос на доступ к камере</h3>
|
||||
<p>Оператор <strong id="operator-id"></strong> запрашивает доступ к камере</p>
|
||||
<div class="request-buttons">
|
||||
<button id="btn-accept" class="btn-accept">✅ Принять</button>
|
||||
<button id="btn-reject" class="btn-reject">❌ Отклонить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="logs" class="logs">
|
||||
<!-- Логи здесь -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
class MobileCameraApp {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.deviceId = this.generateDeviceId();
|
||||
this.localStream = null;
|
||||
this.currentCamera = 'back';
|
||||
this.activeSessions = new Map();
|
||||
this.pendingRequests = new Map();
|
||||
this.peerConnections = new Map(); // WebRTC connections
|
||||
this.logTimeout = null;
|
||||
this.isConnected = false;
|
||||
this.isCameraOn = false;
|
||||
|
||||
this.elements = {
|
||||
status: document.getElementById('status'),
|
||||
localVideo: document.getElementById('localVideo'),
|
||||
noCamera: document.getElementById('no-camera'),
|
||||
deviceId: document.getElementById('device-id'),
|
||||
currentCamera: document.getElementById('current-camera'),
|
||||
sessionCount: document.getElementById('session-count'),
|
||||
requestsPanel: document.getElementById('requests-panel'),
|
||||
operatorId: document.getElementById('operator-id'),
|
||||
logs: document.getElementById('logs'),
|
||||
btnCamera: document.getElementById('btn-camera'),
|
||||
btnSwitch: document.getElementById('btn-switch'),
|
||||
btnDisconnect: document.getElementById('btn-disconnect'),
|
||||
btnAccept: document.getElementById('btn-accept'),
|
||||
btnReject: document.getElementById('btn-reject')
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
generateDeviceId() {
|
||||
// Используем комбинацию для мобильного устройства
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substr(2, 9);
|
||||
return `mobile_${timestamp}_${random}`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.elements.deviceId.textContent = this.deviceId;
|
||||
this.setupEventListeners();
|
||||
this.connectToServer();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.elements.btnCamera.addEventListener('click', () => this.toggleCamera());
|
||||
this.elements.btnSwitch.addEventListener('click', () => this.switchCamera());
|
||||
this.elements.btnDisconnect.addEventListener('click', () => this.disconnect());
|
||||
this.elements.btnAccept.addEventListener('click', () => this.acceptRequest());
|
||||
this.elements.btnReject.addEventListener('click', () => this.rejectRequest());
|
||||
|
||||
// Обработка поворота экрана
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(() => this.adjustLayout(), 100);
|
||||
});
|
||||
}
|
||||
|
||||
connectToServer() {
|
||||
this.log('Подключение к серверу...', 'info');
|
||||
this.socket = io();
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
this.log('✅ Подключено к серверу', 'success');
|
||||
this.isConnected = true;
|
||||
this.updateStatus('connected');
|
||||
this.registerDevice();
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
this.log('❌ Отключено от сервера', 'warning');
|
||||
this.isConnected = false;
|
||||
this.updateStatus('disconnected');
|
||||
this.stopCamera();
|
||||
});
|
||||
|
||||
this.socket.on('camera:request', (data) => {
|
||||
this.handleCameraRequest(data);
|
||||
});
|
||||
|
||||
this.socket.on('camera:switch', (data) => {
|
||||
this.handleCameraSwitchRequest(data);
|
||||
});
|
||||
|
||||
this.socket.on('camera:disconnect', (data) => {
|
||||
this.handleDisconnectRequest(data);
|
||||
});
|
||||
|
||||
// WebRTC обработчики
|
||||
this.socket.on('webrtc:offer', (data) => {
|
||||
this.handleWebRTCOffer(data);
|
||||
});
|
||||
|
||||
this.socket.on('webrtc:answer', (data) => {
|
||||
this.handleWebRTCAnswer(data);
|
||||
});
|
||||
|
||||
this.socket.on('webrtc:ice-candidate', (data) => {
|
||||
this.handleWebRTCIceCandidate(data);
|
||||
});
|
||||
|
||||
this.socket.on('register:success', (data) => {
|
||||
this.log(`📱 Устройство зарегистрировано: ${data.deviceId}`, 'success');
|
||||
});
|
||||
|
||||
this.socket.on('register:error', (error) => {
|
||||
this.log(`❌ Ошибка регистрации: ${error.message}`, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
registerDevice() {
|
||||
const deviceInfo = {
|
||||
platform: 'mobile_web',
|
||||
userAgent: navigator.userAgent,
|
||||
cameras: ['back', 'front'],
|
||||
capabilities: {
|
||||
video: true,
|
||||
audio: false
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.emit('register:mobile_web', {
|
||||
deviceId: this.deviceId,
|
||||
deviceInfo: deviceInfo
|
||||
});
|
||||
}
|
||||
|
||||
async toggleCamera() {
|
||||
if (this.isCameraOn) {
|
||||
this.stopCamera();
|
||||
} else {
|
||||
await this.startCamera();
|
||||
}
|
||||
}
|
||||
|
||||
async startCamera() {
|
||||
try {
|
||||
this.log(`📷 Запуск камеры (${this.currentCamera})...`, 'info');
|
||||
|
||||
const constraints = {
|
||||
video: {
|
||||
facingMode: this.currentCamera === 'front' ? 'user' : 'environment',
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
},
|
||||
audio: false
|
||||
};
|
||||
|
||||
this.localStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
this.elements.localVideo.srcObject = this.localStream;
|
||||
|
||||
this.isCameraOn = true;
|
||||
this.elements.btnCamera.textContent = '📷';
|
||||
this.elements.btnCamera.style.background = '#f44336';
|
||||
this.elements.noCamera.style.display = 'none';
|
||||
this.elements.localVideo.style.display = 'block';
|
||||
|
||||
// Для фронтальной камеры применяем зеркальное отображение
|
||||
this.elements.localVideo.style.transform =
|
||||
this.currentCamera === 'front' ? 'scaleX(-1)' : 'scaleX(1)';
|
||||
|
||||
this.updateCameraInfo();
|
||||
this.log(`✅ Камера ${this.currentCamera} запущена`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
this.log(`❌ Ошибка доступа к камере: ${error.message}`, 'error');
|
||||
this.isCameraOn = false;
|
||||
}
|
||||
}
|
||||
|
||||
stopCamera() {
|
||||
if (this.localStream) {
|
||||
this.localStream.getTracks().forEach(track => track.stop());
|
||||
this.localStream = null;
|
||||
}
|
||||
|
||||
this.isCameraOn = false;
|
||||
this.elements.btnCamera.textContent = '📷';
|
||||
this.elements.btnCamera.style.background = '#4CAF50';
|
||||
this.elements.localVideo.style.display = 'none';
|
||||
this.elements.noCamera.style.display = 'block';
|
||||
|
||||
this.updateCameraInfo();
|
||||
this.log('📷 Камера остановлена', 'info');
|
||||
}
|
||||
|
||||
async switchCamera(targetCamera = null) {
|
||||
if (!this.isCameraOn) {
|
||||
this.log('⚠️ Сначала включите камеру', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Если указан конкретный тип камеры - используем его
|
||||
if (targetCamera) {
|
||||
this.currentCamera = targetCamera;
|
||||
} else {
|
||||
// Иначе переключаем на противоположную
|
||||
this.currentCamera = this.currentCamera === 'front' ? 'back' : 'front';
|
||||
}
|
||||
|
||||
this.log(`🔄 Переключение камеры на: ${this.currentCamera}`, 'info');
|
||||
this.stopCamera();
|
||||
await this.startCamera();
|
||||
|
||||
// Обновляем все активные WebRTC соединения
|
||||
await this.updateWebRTCStreams();
|
||||
}
|
||||
|
||||
handleCameraRequest(data) {
|
||||
const { sessionId, operatorId, cameraType } = data;
|
||||
this.log(`📞 Запрос камеры от оператора ${operatorId}`, 'info');
|
||||
|
||||
this.pendingRequests.set(sessionId, { operatorId, cameraType, sessionId });
|
||||
this.showRequestPanel(operatorId, sessionId);
|
||||
}
|
||||
|
||||
handleCameraSwitchRequest(data) {
|
||||
const { sessionId, cameraType } = data;
|
||||
this.log(`🔄 Запрос переключения камеры: ${cameraType}`, 'info');
|
||||
|
||||
if (this.activeSessions.has(sessionId)) {
|
||||
if (this.isCameraOn) {
|
||||
// Переключаем на конкретно запрошенный тип камеры
|
||||
this.switchCamera(cameraType);
|
||||
} else {
|
||||
// Если камера не включена, просто обновляем тип
|
||||
this.currentCamera = cameraType;
|
||||
this.log(`📝 Тип камеры обновлен на: ${cameraType}`, 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDisconnectRequest(data) {
|
||||
const { sessionId } = data;
|
||||
this.log(`🔌 Запрос отключения сессии: ${sessionId}`, 'info');
|
||||
|
||||
if (this.activeSessions.has(sessionId)) {
|
||||
this.activeSessions.delete(sessionId);
|
||||
this.updateSessionCount();
|
||||
}
|
||||
}
|
||||
|
||||
showRequestPanel(operatorId, sessionId) {
|
||||
this.elements.operatorId.textContent = operatorId;
|
||||
this.elements.requestsPanel.classList.add('visible');
|
||||
this.currentRequestSessionId = sessionId;
|
||||
}
|
||||
|
||||
hideRequestPanel() {
|
||||
this.elements.requestsPanel.classList.remove('visible');
|
||||
this.currentRequestSessionId = null;
|
||||
}
|
||||
|
||||
async acceptRequest() {
|
||||
const sessionId = this.currentRequestSessionId;
|
||||
const requestData = this.pendingRequests.get(sessionId);
|
||||
|
||||
if (!requestData) return;
|
||||
|
||||
this.log(`✅ Принят запрос сессии: ${sessionId}`, 'success');
|
||||
|
||||
// Запускаем камеру если не запущена
|
||||
if (!this.isCameraOn) {
|
||||
this.currentCamera = requestData.cameraType || 'back';
|
||||
await this.startCamera();
|
||||
}
|
||||
|
||||
// Добавляем сессию
|
||||
this.activeSessions.set(sessionId, {
|
||||
operatorId: requestData.operatorId,
|
||||
cameraType: this.currentCamera,
|
||||
startTime: new Date()
|
||||
});
|
||||
|
||||
// Отправляем подтверждение с поддержкой WebRTC
|
||||
this.socket.emit('camera:response', {
|
||||
sessionId: sessionId,
|
||||
accepted: true,
|
||||
streamUrl: 'webrtc', // Указываем что используем WebRTC
|
||||
cameraType: this.currentCamera
|
||||
});
|
||||
|
||||
this.pendingRequests.delete(sessionId);
|
||||
this.hideRequestPanel();
|
||||
this.updateSessionCount();
|
||||
}
|
||||
|
||||
rejectRequest() {
|
||||
const sessionId = this.currentRequestSessionId;
|
||||
const requestData = this.pendingRequests.get(sessionId);
|
||||
|
||||
if (!requestData) return;
|
||||
|
||||
this.log(`❌ Отклонен запрос сессии: ${sessionId}`, 'warning');
|
||||
|
||||
this.socket.emit('camera:response', {
|
||||
sessionId: sessionId,
|
||||
accepted: false,
|
||||
error: 'Пользователь отклонил запрос'
|
||||
});
|
||||
|
||||
this.pendingRequests.delete(sessionId);
|
||||
this.hideRequestPanel();
|
||||
}
|
||||
|
||||
// ===== WebRTC Methods =====
|
||||
async createPeerConnection(sessionId) {
|
||||
const config = {
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' }
|
||||
]
|
||||
};
|
||||
|
||||
const peerConnection = new RTCPeerConnection(config);
|
||||
|
||||
// Добавляем обработчики событий
|
||||
peerConnection.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
this.socket.emit('webrtc:ice-candidate', {
|
||||
sessionId: sessionId,
|
||||
candidate: event.candidate
|
||||
});
|
||||
this.log('🧊 ICE candidate отправлен', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
peerConnection.onconnectionstatechange = () => {
|
||||
this.log(`🔗 WebRTC состояние: ${peerConnection.connectionState}`, 'info');
|
||||
if (peerConnection.connectionState === 'connected') {
|
||||
this.log('✅ WebRTC соединение установлено', 'success');
|
||||
} else if (peerConnection.connectionState === 'failed') {
|
||||
this.log('❌ WebRTC соединение неудачно', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
peerConnection.onicegatheringstatechange = () => {
|
||||
this.log(`❄️ ICE gathering: ${peerConnection.iceGatheringState}`, 'info');
|
||||
};
|
||||
|
||||
// Добавляем локальный поток если есть
|
||||
if (this.localStream) {
|
||||
this.localStream.getTracks().forEach(track => {
|
||||
peerConnection.addTrack(track, this.localStream);
|
||||
this.log(`🎥 Трек добавлен: ${track.kind}`, 'info');
|
||||
});
|
||||
}
|
||||
|
||||
return peerConnection;
|
||||
}
|
||||
|
||||
async handleWebRTCOffer(data) {
|
||||
const { sessionId, offer } = data;
|
||||
this.log(`📞 Получен WebRTC offer для сессии: ${sessionId}`, 'info');
|
||||
|
||||
try {
|
||||
// Создаем новое peer connection для этой сессии
|
||||
const peerConnection = await this.createPeerConnection(sessionId);
|
||||
this.peerConnections.set(sessionId, peerConnection);
|
||||
|
||||
// Устанавливаем remote description
|
||||
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
this.log('📝 Remote description установлен', 'info');
|
||||
|
||||
// Создаем answer
|
||||
const answer = await peerConnection.createAnswer();
|
||||
await peerConnection.setLocalDescription(answer);
|
||||
|
||||
// Отправляем answer
|
||||
this.socket.emit('webrtc:answer', {
|
||||
sessionId: sessionId,
|
||||
answer: answer
|
||||
});
|
||||
|
||||
this.log('✅ WebRTC answer отправлен', 'success');
|
||||
|
||||
} catch (error) {
|
||||
this.log(`❌ Ошибка WebRTC offer: ${error.message}`, 'error');
|
||||
this.socket.emit('webrtc:error', {
|
||||
sessionId: sessionId,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleWebRTCAnswer(data) {
|
||||
const { sessionId, answer } = data;
|
||||
this.log(`📞 Получен WebRTC answer для сессии: ${sessionId}`, 'info');
|
||||
|
||||
try {
|
||||
const peerConnection = this.peerConnections.get(sessionId);
|
||||
if (peerConnection) {
|
||||
await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
|
||||
this.log('✅ WebRTC answer обработан', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`❌ Ошибка WebRTC answer: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async handleWebRTCIceCandidate(data) {
|
||||
const { sessionId, candidate } = data;
|
||||
|
||||
try {
|
||||
const peerConnection = this.peerConnections.get(sessionId);
|
||||
if (peerConnection) {
|
||||
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
|
||||
this.log('🧊 ICE candidate добавлен', 'info');
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`❌ Ошибка ICE candidate: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async updateWebRTCStreams() {
|
||||
if (!this.localStream) {
|
||||
this.log('⚠️ Нет локального потока для обновления', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('🔄 Обновление WebRTC потоков...', 'info');
|
||||
|
||||
// Обновляем все активные peer connections
|
||||
for (const [sessionId, peerConnection] of this.peerConnections.entries()) {
|
||||
try {
|
||||
// Удаляем старые треки
|
||||
const senders = peerConnection.getSenders();
|
||||
for (const sender of senders) {
|
||||
if (sender.track) {
|
||||
peerConnection.removeTrack(sender);
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем новые треки
|
||||
this.localStream.getTracks().forEach(track => {
|
||||
peerConnection.addTrack(track, this.localStream);
|
||||
});
|
||||
|
||||
this.log(`✅ WebRTC поток обновлен для сессии: ${sessionId}`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
this.log(`❌ Ошибка обновления WebRTC потока: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.log('🔌 Отключение от сервера...', 'info');
|
||||
this.stopCamera();
|
||||
|
||||
// Закрываем все WebRTC соединения
|
||||
this.peerConnections.forEach((pc, sessionId) => {
|
||||
pc.close();
|
||||
this.log(`🔒 WebRTC соединение закрыто: ${sessionId}`, 'info');
|
||||
});
|
||||
this.peerConnections.clear();
|
||||
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(status) {
|
||||
this.elements.status.textContent = status === 'connected' ? 'Подключено' : 'Отключено';
|
||||
this.elements.status.className = `status ${status}`;
|
||||
}
|
||||
|
||||
updateCameraInfo() {
|
||||
this.elements.currentCamera.textContent =
|
||||
this.isCameraOn ? this.currentCamera : 'none';
|
||||
}
|
||||
|
||||
updateSessionCount() {
|
||||
this.elements.sessionCount.textContent = this.activeSessions.size;
|
||||
}
|
||||
|
||||
adjustLayout() {
|
||||
// Адаптация под поворот экрана
|
||||
setTimeout(() => {
|
||||
if (this.localStream && this.elements.localVideo.srcObject) {
|
||||
this.elements.localVideo.play();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
log(message, type = 'info') {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry ${type}`;
|
||||
logEntry.textContent = `[${timestamp}] ${message}`;
|
||||
|
||||
this.elements.logs.appendChild(logEntry);
|
||||
this.elements.logs.scrollTop = this.elements.logs.scrollHeight;
|
||||
|
||||
// Показываем логи на 3 секунды
|
||||
this.elements.logs.classList.add('visible');
|
||||
clearTimeout(this.logTimeout);
|
||||
this.logTimeout = setTimeout(() => {
|
||||
this.elements.logs.classList.remove('visible');
|
||||
}, 3000);
|
||||
|
||||
console.log(`[Mobile] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Запуск приложения
|
||||
let app;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
app = new MobileCameraApp();
|
||||
});
|
||||
|
||||
// Предотвращение случайного обновления страницы
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
if (app && app.isConnected) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
return '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
486
backend/src/managers/ConnectionManager.js
Normal file
486
backend/src/managers/ConnectionManager.js
Normal file
@@ -0,0 +1,486 @@
|
||||
/**
|
||||
* ConnectionManager - управляет подключениями между операторами и устройствами
|
||||
*/
|
||||
class ConnectionManager {
|
||||
constructor(sessionManager, deviceManager, logger) {
|
||||
this.sessionManager = sessionManager;
|
||||
this.deviceManager = deviceManager;
|
||||
this.logger = logger;
|
||||
this.connectionRequests = new Map(); // requestId -> ConnectionRequest
|
||||
this.activeConnections = new Map(); // connectionId -> Connection
|
||||
this.connectionTimeouts = new Map(); // connectionId -> timeoutId
|
||||
this.maxConnectionsPerDevice = 1; // Ограничение: одно соединение на устройство
|
||||
this.connectionTimeout = 30000; // 30 секунд на установку соединения
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициация подключения оператора к устройству
|
||||
* @param {string} operatorId
|
||||
* @param {string} deviceId
|
||||
* @param {string} cameraType
|
||||
* @returns {Promise<object>} {success: boolean, connectionId?: string, sessionId?: string, error?: string}
|
||||
*/
|
||||
async initiateConnection(operatorId, deviceId, cameraType = 'back') {
|
||||
this.logger.info(`🔗 Initiating connection: ${operatorId} -> ${deviceId} (${cameraType})`);
|
||||
|
||||
// Проверяем возможность создания соединения
|
||||
const validation = this.deviceManager.canCreateSession(deviceId, operatorId);
|
||||
if (!validation.canConnect) {
|
||||
this.logger.error(`❌ Connection validation failed: ${validation.reason}`);
|
||||
throw new Error(validation.reason);
|
||||
}
|
||||
|
||||
// Создаем сессию
|
||||
const session = this.sessionManager.createSession(deviceId, operatorId, cameraType);
|
||||
const connectionId = session.sessionId;
|
||||
|
||||
// Создаем запрос на подключение
|
||||
const connectionRequest = {
|
||||
connectionId,
|
||||
sessionId: connectionId, // Для совместимости
|
||||
operatorId,
|
||||
deviceId,
|
||||
cameraType,
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
timeoutAt: new Date(Date.now() + this.connectionTimeout),
|
||||
// Для прямого WebRTC соединения
|
||||
webrtc: {
|
||||
signalingCompleted: false,
|
||||
directConnection: false,
|
||||
state: 'initialized',
|
||||
stunServers: ['stun:stun.l.google.com:19302'] // STUN серверы для NAT traversal
|
||||
}
|
||||
};
|
||||
|
||||
this.connectionRequests.set(connectionId, connectionRequest);
|
||||
|
||||
// Устанавливаем таймаут
|
||||
const timeoutId = setTimeout(async () => {
|
||||
await this.handleConnectionTimeout(connectionId);
|
||||
}, this.connectionTimeout);
|
||||
|
||||
this.connectionTimeouts.set(connectionId, timeoutId);
|
||||
|
||||
// Отправляем запрос Android устройству
|
||||
const device = this.deviceManager.getDevice(deviceId);
|
||||
if (!device || !device.isConnected()) {
|
||||
this.logger.error(`❌ Device not connected: ${deviceId}`);
|
||||
this.connectionRequests.delete(connectionId);
|
||||
clearTimeout(timeoutId);
|
||||
this.connectionTimeouts.delete(connectionId);
|
||||
throw new Error('Device not connected');
|
||||
}
|
||||
|
||||
const requestData = {
|
||||
sessionId: connectionId,
|
||||
operatorId: operatorId,
|
||||
cameraType: cameraType
|
||||
};
|
||||
|
||||
this.logger.info(`📱 Sending camera:request to Android device ${deviceId}`);
|
||||
device.socket.emit('camera:request', requestData);
|
||||
|
||||
// Добавляем сессию к участникам
|
||||
device.addSession(connectionId);
|
||||
const operator = this.deviceManager.getOperator(operatorId);
|
||||
if (operator) {
|
||||
operator.addSession(connectionId);
|
||||
}
|
||||
|
||||
this.logger.info(`✅ Connection request created: ${connectionId}`);
|
||||
return { success: true, connectionId, sessionId: connectionId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Принятие запроса на подключение от устройства
|
||||
* @param {string} connectionId
|
||||
* @param {object} connectionData
|
||||
* @returns {Promise<object>} {success: boolean, error?: string}
|
||||
*/
|
||||
async acceptConnection(connectionId, connectionData = {}) {
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
if (!request) {
|
||||
this.logger.error(`❌ Connection request not found: ${connectionId}`);
|
||||
throw new Error('Connection request not found');
|
||||
}
|
||||
|
||||
// Очищаем таймаут
|
||||
this.clearConnectionTimeout(connectionId);
|
||||
|
||||
// Создаем активное соединение
|
||||
const connection = {
|
||||
connectionId,
|
||||
operatorId: request.operatorId,
|
||||
deviceId: request.deviceId,
|
||||
cameraType: request.cameraType,
|
||||
status: 'active',
|
||||
establishedAt: new Date(),
|
||||
streamUrl: connectionData.streamUrl || 'webrtc',
|
||||
lastActivity: new Date()
|
||||
};
|
||||
|
||||
this.activeConnections.set(connectionId, connection);
|
||||
this.connectionRequests.delete(connectionId);
|
||||
|
||||
// Обновляем сессию
|
||||
const session = this.sessionManager.getSession(connectionId);
|
||||
if (session) {
|
||||
session.updateStatus('active', { streamUrl: connection.streamUrl });
|
||||
}
|
||||
|
||||
this.logger.info(`✅ Connection established: ${connectionId}`);
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Отклонение запроса на подключение
|
||||
* @param {string} connectionId
|
||||
* @param {string} reason
|
||||
* @returns {Promise<object>} {success: boolean, error?: string}
|
||||
*/
|
||||
async rejectConnection(connectionId, reason = 'User rejected') {
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
if (!request) {
|
||||
this.logger.error(`❌ Connection request not found: ${connectionId}`);
|
||||
throw new Error('Connection request not found');
|
||||
}
|
||||
|
||||
// Очищаем таймаут
|
||||
this.clearConnectionTimeout(connectionId);
|
||||
|
||||
// Обновляем сессию
|
||||
const session = this.sessionManager.getSession(connectionId);
|
||||
if (session) {
|
||||
session.updateStatus('denied', { error: reason });
|
||||
}
|
||||
|
||||
this.connectionRequests.delete(connectionId);
|
||||
|
||||
this.logger.info(`❌ Connection rejected: ${connectionId} - ${reason}`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение активного соединения
|
||||
* @param {string} connectionId
|
||||
* @param {string} reason
|
||||
* @returns {Promise<object>} {success: boolean, error?: string}
|
||||
*/
|
||||
async terminateConnection(connectionId, reason = 'Connection terminated') {
|
||||
// Ищем соединение в активных или ожидающих
|
||||
const connection = this.activeConnections.get(connectionId);
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
|
||||
if (!connection && !request) {
|
||||
this.logger.error(`❌ Connection not found: ${connectionId}`);
|
||||
throw new Error('Connection not found');
|
||||
}
|
||||
|
||||
// Закрываем сессию
|
||||
this.sessionManager.closeSession(connectionId);
|
||||
|
||||
// Удаляем из соответствующих коллекций
|
||||
if (connection) {
|
||||
this.activeConnections.delete(connectionId);
|
||||
}
|
||||
if (request) {
|
||||
this.connectionRequests.delete(connectionId);
|
||||
}
|
||||
|
||||
// Очищаем устройство и оператора
|
||||
const target = connection || request;
|
||||
const device = this.deviceManager.getDevice(target.deviceId);
|
||||
const operator = this.deviceManager.getOperator(target.operatorId);
|
||||
|
||||
if (device) {
|
||||
device.removeSession(connectionId);
|
||||
}
|
||||
if (operator) {
|
||||
operator.removeSession(connectionId);
|
||||
}
|
||||
|
||||
this.logger.info(`🔌 Connection terminated: ${connectionId} - ${reason}`);
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка таймаута соединения
|
||||
* @param {string} connectionId
|
||||
*/
|
||||
async handleConnectionTimeout(connectionId) {
|
||||
this.logger.warn(`⏰ Connection timeout: ${connectionId}`);
|
||||
|
||||
try {
|
||||
// Проверяем есть ли запрос перед отклонением
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
if (request) {
|
||||
await this.rejectConnection(connectionId, 'Connection timeout');
|
||||
} else {
|
||||
this.logger.info(`Connection ${connectionId} already removed from pending requests`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error handling timeout for ${connectionId}:`, error.message);
|
||||
}
|
||||
|
||||
// Уведомляем участников о таймауте
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
if (request) {
|
||||
const operator = this.deviceManager.getOperator(request.operatorId);
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('connection:timeout', {
|
||||
connectionId,
|
||||
deviceId: request.deviceId,
|
||||
error: 'Connection request timeout'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка таймаута соединения
|
||||
* @param {string} connectionId
|
||||
*/
|
||||
clearConnectionTimeout(connectionId) {
|
||||
const timeoutId = this.connectionTimeouts.get(connectionId);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
this.connectionTimeouts.delete(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статистики соединений
|
||||
* @returns {object}
|
||||
*/
|
||||
getConnectionStats() {
|
||||
return {
|
||||
pendingRequests: this.connectionRequests.size,
|
||||
activeConnections: this.activeConnections.size,
|
||||
totalRequestsProcessed: this.connectionRequests.size + this.activeConnections.size,
|
||||
averageConnectionTime: this.calculateAverageConnectionTime()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Расчет среднего времени установки соединения
|
||||
* @returns {number} время в миллисекундах
|
||||
*/
|
||||
calculateAverageConnectionTime() {
|
||||
if (this.activeConnections.size === 0) return 0;
|
||||
|
||||
let totalTime = 0;
|
||||
let count = 0;
|
||||
|
||||
for (const connection of this.activeConnections.values()) {
|
||||
if (connection.establishedAt) {
|
||||
// Примерное время установки соединения (можно улучшить, сохраняя время запроса)
|
||||
totalTime += 2000; // placeholder
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count > 0 ? totalTime / count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение активного соединения
|
||||
* @param {string} connectionId
|
||||
* @returns {object|null}
|
||||
*/
|
||||
getConnection(connectionId) {
|
||||
return this.activeConnections.get(connectionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех активных соединений для оператора
|
||||
* @param {string} operatorId
|
||||
* @returns {Array}
|
||||
*/
|
||||
getOperatorConnections(operatorId) {
|
||||
return Array.from(this.activeConnections.values())
|
||||
.filter(conn => conn.operatorId === operatorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех активных соединений для устройства
|
||||
* @param {string} deviceId
|
||||
* @returns {Array}
|
||||
*/
|
||||
getDeviceConnections(deviceId) {
|
||||
return Array.from(this.activeConnections.values())
|
||||
.filter(conn => conn.deviceId === deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка устаревших запросов и неактивных соединений
|
||||
*/
|
||||
async cleanup() {
|
||||
const now = new Date();
|
||||
|
||||
// Очищаем устаревшие запросы
|
||||
for (const [connectionId, request] of this.connectionRequests.entries()) {
|
||||
if (now > request.timeoutAt) {
|
||||
await this.handleConnectionTimeout(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем активные соединения на отключенные сокеты
|
||||
for (const [connectionId, connection] of this.activeConnections.entries()) {
|
||||
const device = this.deviceManager.getDevice(connection.deviceId);
|
||||
const operator = this.deviceManager.getOperator(connection.operatorId);
|
||||
|
||||
if (!device || !device.isConnected() || !operator || !operator.isConnected()) {
|
||||
try {
|
||||
await this.terminateConnection(connectionId, 'Participant disconnected');
|
||||
} catch (error) {
|
||||
this.logger.error(`Error terminating connection ${connectionId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(`🧹 Connection cleanup completed. Active: ${this.activeConnections.size}, Pending: ${this.connectionRequests.size}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка всех подключений устройства при его отключении
|
||||
* @param {string} deviceId
|
||||
*/
|
||||
async cleanupDeviceConnections(deviceId) {
|
||||
this.logger.info(`🧹 Cleaning up connections for device: ${deviceId}`);
|
||||
|
||||
const connectionsToTerminate = [];
|
||||
|
||||
// Находим все активные подключения устройства
|
||||
for (const [connectionId, connection] of this.activeConnections.entries()) {
|
||||
if (connection.deviceId === deviceId) {
|
||||
connectionsToTerminate.push(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Находим все ожидающие запросы для устройства
|
||||
for (const [connectionId, request] of this.connectionRequests.entries()) {
|
||||
if (request.deviceId === deviceId) {
|
||||
connectionsToTerminate.push(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Завершаем все найденные подключения
|
||||
for (const connectionId of connectionsToTerminate) {
|
||||
try {
|
||||
await this.terminateConnection(connectionId, 'Device disconnected');
|
||||
} catch (error) {
|
||||
this.logger.error(`Error terminating connection ${connectionId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`🧹 Cleaned up ${connectionsToTerminate.length} connections for device ${deviceId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка всех подключений оператора при его отключении
|
||||
* @param {string} operatorId
|
||||
*/
|
||||
async cleanupOperatorConnections(operatorId) {
|
||||
this.logger.info(`🧹 Cleaning up connections for operator: ${operatorId}`);
|
||||
|
||||
const connectionsToTerminate = [];
|
||||
|
||||
// Находим все активные подключения оператора
|
||||
for (const [connectionId, connection] of this.activeConnections.entries()) {
|
||||
if (connection.operatorId === operatorId) {
|
||||
connectionsToTerminate.push(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Находим все ожидающие запросы от оператора
|
||||
for (const [connectionId, request] of this.connectionRequests.entries()) {
|
||||
if (request.operatorId === operatorId) {
|
||||
connectionsToTerminate.push(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Завершаем все найденные подключения
|
||||
for (const connectionId of connectionsToTerminate) {
|
||||
try {
|
||||
await this.terminateConnection(connectionId, 'Operator disconnected');
|
||||
} catch (error) {
|
||||
this.logger.error(`Error terminating connection ${connectionId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`🧹 Cleaned up ${connectionsToTerminate.length} connections for operator ${operatorId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление WebRTC состояния соединения
|
||||
* @param {string} connectionId
|
||||
* @param {string} state - offer_sent, answer_sent, ice_completed, connected
|
||||
*/
|
||||
updateWebRTCState(connectionId, state) {
|
||||
const connection = this.activeConnections.get(connectionId);
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
|
||||
const target = connection || request;
|
||||
if (!target || !target.webrtc) {
|
||||
this.logger.error(`❌ Cannot update WebRTC state for connection: ${connectionId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
target.webrtc.state = state;
|
||||
target.webrtc.lastUpdated = new Date();
|
||||
|
||||
// Если соединение установлено, переходим в режим прямого соединения
|
||||
if (state === 'connected') {
|
||||
target.webrtc.signalingCompleted = true;
|
||||
target.webrtc.directConnection = true;
|
||||
this.logger.info(`🔗 WebRTC direct connection established: ${connectionId}`);
|
||||
}
|
||||
|
||||
this.logger.info(`🔄 WebRTC state updated: ${connectionId} -> ${state}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка готовности к прямому соединению
|
||||
* @param {string} connectionId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isDirectConnectionReady(connectionId) {
|
||||
const connection = this.activeConnections.get(connectionId);
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
|
||||
const target = connection || request;
|
||||
if (!target || !target.webrtc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return target.webrtc.signalingCompleted && target.webrtc.directConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение WebRTC информации соединения
|
||||
* @param {string} connectionId
|
||||
* @returns {object|null}
|
||||
*/
|
||||
getWebRTCInfo(connectionId) {
|
||||
const connection = this.activeConnections.get(connectionId);
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
|
||||
const target = connection || request;
|
||||
if (!target || !target.webrtc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
connectionId,
|
||||
state: target.webrtc.state,
|
||||
signalingCompleted: target.webrtc.signalingCompleted,
|
||||
directConnection: target.webrtc.directConnection,
|
||||
lastUpdated: target.webrtc.lastUpdated,
|
||||
stunServers: target.webrtc.stunServers
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ConnectionManager };
|
||||
515
backend/src/managers/ConnectionManager.js.backup
Normal file
515
backend/src/managers/ConnectionManager.js.backup
Normal file
@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* ConnectionManager - управляет подключениями между операторами и устройствами
|
||||
*/
|
||||
class ConnectionManager {
|
||||
constructor(sessionManager, deviceManager, logger) {
|
||||
this.sessionManager = sessionManager;
|
||||
this.deviceManager = deviceManager;
|
||||
this.logger = logger;
|
||||
this.connectionRequests = new Map(); // requestId -> ConnectionRequest
|
||||
this.activeConnections = new Map();
|
||||
this.logger.info(`🧹 Cleaned up ${connectionsToTerminate.length} connections for operator ${operatorId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление WebRTC состояния соединения
|
||||
* @param {string} connectionId
|
||||
* @param {string} state - offer_sent, answer_sent, ice_completed, direct_established
|
||||
*/
|
||||
updateWebRTCState(connectionId, state) {
|
||||
const connection = this.activeConnections.get(connectionId);
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
|
||||
if (connection && connection.webrtc) {
|
||||
connection.webrtc.state = state;
|
||||
connection.webrtc.lastUpdated = new Date();
|
||||
|
||||
// Если ICE candidates обменялись, начинаем переход на прямое соединение
|
||||
if (state === 'ice_completed') {
|
||||
this.initiateDirectConnection(connectionId);
|
||||
}
|
||||
|
||||
this.logger.info(`🔄 WebRTC state updated: ${connectionId} -> ${state}`);
|
||||
} else if (request && request.webrtc) {
|
||||
request.webrtc.state = state;
|
||||
request.webrtc.lastUpdated = new Date();
|
||||
|
||||
this.logger.info(`🔄 WebRTC state updated (pending): ${connectionId} -> ${state}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициация прямого соединения после завершения WebRTC сигналинга
|
||||
* @param {string} connectionId
|
||||
*/
|
||||
async initiateDirectConnection(connectionId) {
|
||||
const connection = this.activeConnections.get(connectionId);
|
||||
if (!connection) {
|
||||
this.logger.error(`❌ Connection not found for direct setup: ${connectionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(`🔗 Initiating direct WebRTC connection: ${connectionId}`);
|
||||
|
||||
try {
|
||||
// Получаем устройство и оператора
|
||||
const device = this.deviceManager.getDevice(connection.deviceId);
|
||||
const operator = this.deviceManager.getOperator(connection.operatorId);
|
||||
|
||||
if (!device || !device.isConnected() || !operator || !operator.isConnected()) {
|
||||
throw new Error('Device or operator not connected');
|
||||
}
|
||||
|
||||
// Отправляем уведомление о переходе на прямое соединение
|
||||
device.socket.emit('webrtc:direct-mode', {
|
||||
connectionId,
|
||||
message: 'Switching to direct WebRTC connection'
|
||||
});
|
||||
|
||||
operator.socket.emit('webrtc:direct-mode', {
|
||||
connectionId,
|
||||
message: 'Switching to direct WebRTC connection'
|
||||
});
|
||||
|
||||
// Обновляем статус
|
||||
connection.webrtc.directConnection = true;
|
||||
connection.webrtc.signalingCompleted = true;
|
||||
|
||||
this.logger.info(`✅ Direct WebRTC connection initiated: ${connectionId}`);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`❌ Error initiating direct connection: ${connectionId}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранение endpoint информации для прямого соединения
|
||||
* @param {string} connectionId
|
||||
* @param {string} type - 'operator' or 'device'
|
||||
* @param {object} endpoint - {ip, port, candidates}
|
||||
*/
|
||||
setEndpoint(connectionId, type, endpoint) {
|
||||
const connection = this.activeConnections.get(connectionId);
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
|
||||
const target = connection || request;
|
||||
if (!target || !target.webrtc) {
|
||||
this.logger.error(`❌ Cannot set endpoint for connection: ${connectionId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type === 'operator') {
|
||||
target.webrtc.operatorEndpoint = endpoint;
|
||||
} else if (type === 'device') {
|
||||
target.webrtc.deviceEndpoint = endpoint;
|
||||
}
|
||||
|
||||
this.logger.info(`📍 Endpoint set for ${type}: ${connectionId}`, endpoint);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка готовности к прямому соединению
|
||||
* @param {string} connectionId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isReadyForDirectConnection(connectionId) {
|
||||
const connection = this.activeConnections.get(connectionId);
|
||||
if (!connection || !connection.webrtc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return connection.webrtc.operatorEndpoint &&
|
||||
connection.webrtc.deviceEndpoint &&
|
||||
connection.webrtc.signalingCompleted;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionManager;nnectionId -> Connection
|
||||
this.connectionTimeouts = new Map(); // connectionId -> timeoutId
|
||||
this.maxConnectionsPerDevice = 1; // Ограничение: одно соединение на устройство
|
||||
this.connectionTimeout = 30000; // 30 секунд на установку соединения
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициация подключения оператора к устройству
|
||||
* @param {string} operatorId
|
||||
* @param {string} deviceId
|
||||
* @param {string} cameraType
|
||||
* @returns {Promise<object>} {success: boolean, connectionId?: string, sessionId?: string, error?: string}
|
||||
*/
|
||||
async initiateConnection(operatorId, deviceId, cameraType = 'back') {
|
||||
this.logger.info(`🔗 Initiating connection: ${operatorId} -> ${deviceId} (${cameraType})`);
|
||||
|
||||
// Проверяем возможность создания соединения
|
||||
const validation = this.deviceManager.canCreateSession(deviceId, operatorId);
|
||||
if (!validation.canConnect) {
|
||||
this.logger.error(`❌ Connection validation failed: ${validation.reason}`);
|
||||
throw new Error(validation.reason);
|
||||
}
|
||||
|
||||
// Создаем сессию
|
||||
const session = this.sessionManager.createSession(deviceId, operatorId, cameraType);
|
||||
const connectionId = session.sessionId;
|
||||
|
||||
// Создаем запрос на подключение
|
||||
const connectionRequest = {
|
||||
connectionId,
|
||||
sessionId: connectionId, // Для совместимости
|
||||
operatorId,
|
||||
deviceId,
|
||||
cameraType,
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
timeoutAt: new Date(Date.now() + this.connectionTimeout),
|
||||
// Для прямого WebRTC соединения
|
||||
webrtc: {
|
||||
signalingCompleted: false,
|
||||
directConnection: false,
|
||||
operatorEndpoint: null, // IP:port оператора
|
||||
deviceEndpoint: null, // IP:port устройства
|
||||
stunServers: ['stun:stun.l.google.com:19302'] // STUN серверы для NAT traversal
|
||||
}
|
||||
};
|
||||
|
||||
this.connectionRequests.set(connectionId, connectionRequest);
|
||||
|
||||
// Устанавливаем таймаут
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.handleConnectionTimeout(connectionId);
|
||||
}, this.connectionTimeout);
|
||||
|
||||
this.connectionTimeouts.set(connectionId, timeoutId);
|
||||
|
||||
// Отправляем запрос Android устройству
|
||||
const device = this.deviceManager.getDevice(deviceId);
|
||||
if (!device || !device.isConnected()) {
|
||||
this.logger.error(`❌ Device not connected: ${deviceId}`);
|
||||
this.connectionRequests.delete(connectionId);
|
||||
clearTimeout(timeoutId);
|
||||
this.connectionTimeouts.delete(connectionId);
|
||||
throw new Error('Device not connected');
|
||||
}
|
||||
|
||||
const requestData = {
|
||||
sessionId: connectionId,
|
||||
operatorId: operatorId,
|
||||
cameraType: cameraType
|
||||
};
|
||||
|
||||
this.logger.info(`📱 Sending camera:request to Android device ${deviceId}`);
|
||||
device.socket.emit('camera:request', requestData);
|
||||
|
||||
// Добавляем сессию к участникам
|
||||
device.addSession(connectionId);
|
||||
const operator = this.deviceManager.getOperator(operatorId);
|
||||
if (operator) {
|
||||
operator.addSession(connectionId);
|
||||
}
|
||||
|
||||
this.logger.info(`✅ Connection request created: ${connectionId}`);
|
||||
return { success: true, connectionId, sessionId: connectionId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Принятие запроса на подключение от устройства
|
||||
* @param {string} connectionId
|
||||
* @param {object} connectionData
|
||||
* @returns {Promise<object>} {success: boolean, error?: string}
|
||||
*/
|
||||
async acceptConnection(connectionId, connectionData = {}) {
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
if (!request) {
|
||||
this.logger.error(`❌ Connection request not found: ${connectionId}`);
|
||||
throw new Error('Connection request not found');
|
||||
}
|
||||
|
||||
// Очищаем таймаут
|
||||
this.clearConnectionTimeout(connectionId);
|
||||
|
||||
// Создаем активное соединение
|
||||
const connection = {
|
||||
connectionId,
|
||||
operatorId: request.operatorId,
|
||||
deviceId: request.deviceId,
|
||||
cameraType: request.cameraType,
|
||||
status: 'active',
|
||||
establishedAt: new Date(),
|
||||
streamUrl: connectionData.streamUrl || 'webrtc',
|
||||
lastActivity: new Date()
|
||||
};
|
||||
|
||||
this.activeConnections.set(connectionId, connection);
|
||||
this.connectionRequests.delete(connectionId);
|
||||
|
||||
// Обновляем сессию
|
||||
const session = this.sessionManager.getSession(connectionId);
|
||||
if (session) {
|
||||
session.updateStatus('active', { streamUrl: connection.streamUrl });
|
||||
}
|
||||
|
||||
this.logger.info(`✅ Connection established: ${connectionId}`);
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Отклонение запроса на подключение
|
||||
* @param {string} connectionId
|
||||
* @param {string} reason
|
||||
* @returns {Promise<object>} {success: boolean, error?: string}
|
||||
*/
|
||||
async rejectConnection(connectionId, reason = 'User rejected') {
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
if (!request) {
|
||||
this.logger.error(`❌ Connection request not found: ${connectionId}`);
|
||||
throw new Error('Connection request not found');
|
||||
}
|
||||
|
||||
// Очищаем таймаут
|
||||
this.clearConnectionTimeout(connectionId);
|
||||
|
||||
// Обновляем сессию
|
||||
const session = this.sessionManager.getSession(connectionId);
|
||||
if (session) {
|
||||
session.updateStatus('denied', { error: reason });
|
||||
}
|
||||
|
||||
this.connectionRequests.delete(connectionId);
|
||||
|
||||
this.logger.info(`❌ Connection rejected: ${connectionId} - ${reason}`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение активного соединения
|
||||
* @param {string} connectionId
|
||||
* @param {string} reason
|
||||
* @returns {Promise<object>} {success: boolean, error?: string}
|
||||
*/
|
||||
async terminateConnection(connectionId, reason = 'Connection terminated') {
|
||||
const connection = this.activeConnections.get(connectionId);
|
||||
if (!connection) {
|
||||
this.logger.error(`❌ Active connection not found: ${connectionId}`);
|
||||
throw new Error('Active connection not found');
|
||||
}
|
||||
|
||||
// Закрываем сессию
|
||||
this.sessionManager.closeSession(connectionId);
|
||||
|
||||
// Удаляем из активных соединений
|
||||
this.activeConnections.delete(connectionId);
|
||||
|
||||
// Очищаем устройство и оператора
|
||||
const device = this.deviceManager.getDevice(connection.deviceId);
|
||||
const operator = this.deviceManager.getOperator(connection.operatorId);
|
||||
|
||||
if (device) {
|
||||
device.removeSession(connectionId);
|
||||
}
|
||||
if (operator) {
|
||||
operator.removeSession(connectionId);
|
||||
}
|
||||
|
||||
this.logger.info(`🔌 Connection terminated: ${connectionId} - ${reason}`);
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка таймаута соединения
|
||||
* @param {string} connectionId
|
||||
*/
|
||||
handleConnectionTimeout(connectionId) {
|
||||
this.logger.warn(`⏰ Connection timeout: ${connectionId}`);
|
||||
this.rejectConnection(connectionId, 'Connection timeout');
|
||||
|
||||
// Уведомляем участников о таймауте
|
||||
const request = this.connectionRequests.get(connectionId);
|
||||
if (request) {
|
||||
const operator = this.deviceManager.getOperator(request.operatorId);
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('connection:timeout', {
|
||||
connectionId,
|
||||
deviceId: request.deviceId,
|
||||
error: 'Connection request timeout'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка таймаута соединения
|
||||
* @param {string} connectionId
|
||||
*/
|
||||
clearConnectionTimeout(connectionId) {
|
||||
const timeoutId = this.connectionTimeouts.get(connectionId);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
this.connectionTimeouts.delete(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статистики соединений
|
||||
* @returns {object}
|
||||
*/
|
||||
getConnectionStats() {
|
||||
return {
|
||||
pendingRequests: this.connectionRequests.size,
|
||||
activeConnections: this.activeConnections.size,
|
||||
totalRequestsProcessed: this.connectionRequests.size + this.activeConnections.size,
|
||||
averageConnectionTime: this.calculateAverageConnectionTime()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Расчет среднего времени установки соединения
|
||||
* @returns {number} время в миллисекундах
|
||||
*/
|
||||
calculateAverageConnectionTime() {
|
||||
if (this.activeConnections.size === 0) return 0;
|
||||
|
||||
let totalTime = 0;
|
||||
let count = 0;
|
||||
|
||||
for (const connection of this.activeConnections.values()) {
|
||||
if (connection.establishedAt) {
|
||||
// Примерное время установки соединения (можно улучшить, сохраняя время запроса)
|
||||
totalTime += 2000; // placeholder
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count > 0 ? totalTime / count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение активного соединения
|
||||
* @param {string} connectionId
|
||||
* @returns {object|null}
|
||||
*/
|
||||
getConnection(connectionId) {
|
||||
return this.activeConnections.get(connectionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех активных соединений для оператора
|
||||
* @param {string} operatorId
|
||||
* @returns {Array}
|
||||
*/
|
||||
getOperatorConnections(operatorId) {
|
||||
return Array.from(this.activeConnections.values())
|
||||
.filter(conn => conn.operatorId === operatorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех активных соединений для устройства
|
||||
* @param {string} deviceId
|
||||
* @returns {Array}
|
||||
*/
|
||||
getDeviceConnections(deviceId) {
|
||||
return Array.from(this.activeConnections.values())
|
||||
.filter(conn => conn.deviceId === deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка устаревших запросов и неактивных соединений
|
||||
*/
|
||||
async cleanup() {
|
||||
const now = new Date();
|
||||
|
||||
// Очищаем устаревшие запросы
|
||||
for (const [connectionId, request] of this.connectionRequests.entries()) {
|
||||
if (now > request.timeoutAt) {
|
||||
this.handleConnectionTimeout(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем активные соединения на отключенные сокеты
|
||||
for (const [connectionId, connection] of this.activeConnections.entries()) {
|
||||
const device = this.deviceManager.getDevice(connection.deviceId);
|
||||
const operator = this.deviceManager.getOperator(connection.operatorId);
|
||||
|
||||
if (!device || !device.isConnected() || !operator || !operator.isConnected()) {
|
||||
try {
|
||||
await this.terminateConnection(connectionId, 'Participant disconnected');
|
||||
} catch (error) {
|
||||
this.logger.error(`Error terminating connection ${connectionId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(`🧹 Connection cleanup completed. Active: ${this.activeConnections.size}, Pending: ${this.connectionRequests.size}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка всех подключений устройства при его отключении
|
||||
* @param {string} deviceId
|
||||
*/
|
||||
async cleanupDeviceConnections(deviceId) {
|
||||
this.logger.info(`🧹 Cleaning up connections for device: ${deviceId}`);
|
||||
|
||||
const connectionsToTerminate = [];
|
||||
|
||||
// Находим все активные подключения устройства
|
||||
for (const [connectionId, connection] of this.activeConnections.entries()) {
|
||||
if (connection.deviceId === deviceId) {
|
||||
connectionsToTerminate.push(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Находим все ожидающие запросы для устройства
|
||||
for (const [connectionId, request] of this.connectionRequests.entries()) {
|
||||
if (request.deviceId === deviceId) {
|
||||
connectionsToTerminate.push(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Завершаем все найденные подключения
|
||||
for (const connectionId of connectionsToTerminate) {
|
||||
try {
|
||||
await this.terminateConnection(connectionId, 'Device disconnected');
|
||||
} catch (error) {
|
||||
this.logger.error(`Error terminating connection ${connectionId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`🧹 Cleaned up ${connectionsToTerminate.length} connections for device ${deviceId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка всех подключений оператора при его отключении
|
||||
* @param {string} operatorId
|
||||
*/
|
||||
async cleanupOperatorConnections(operatorId) {
|
||||
this.logger.info(`🧹 Cleaning up connections for operator: ${operatorId}`);
|
||||
|
||||
const connectionsToTerminate = [];
|
||||
|
||||
// Находим все активные подключения оператора
|
||||
for (const [connectionId, connection] of this.activeConnections.entries()) {
|
||||
if (connection.operatorId === operatorId) {
|
||||
connectionsToTerminate.push(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Находим все ожидающие запросы от оператора
|
||||
for (const [connectionId, request] of this.connectionRequests.entries()) {
|
||||
if (request.operatorId === operatorId) {
|
||||
connectionsToTerminate.push(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Завершаем все найденные подключения
|
||||
for (const connectionId of connectionsToTerminate) {
|
||||
try {
|
||||
await this.terminateConnection(connectionId, 'Operator disconnected');
|
||||
} catch (error) {
|
||||
this.logger.error(`Error terminating connection ${connectionId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`🧹 Cleaned up ${connectionsToTerminate.length} connections for operator ${operatorId}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ConnectionManager };
|
||||
@@ -12,9 +12,29 @@ class DeviceManager {
|
||||
* @returns {DeviceInfo}
|
||||
*/
|
||||
registerDevice(deviceId, deviceInfo, socket) {
|
||||
const device = new DeviceInfo(deviceId, deviceInfo, socket);
|
||||
this.devices.set(deviceId, device);
|
||||
return device;
|
||||
// Проверяем, существует ли уже устройство с этим ID
|
||||
const existingDevice = this.devices.get(deviceId);
|
||||
|
||||
if (existingDevice) {
|
||||
// Обновляем существующее устройство при переподключении
|
||||
existingDevice.socket = socket;
|
||||
existingDevice.updateStatus('connected');
|
||||
existingDevice.connectedAt = Date.now();
|
||||
existingDevice.lastSeen = Date.now();
|
||||
|
||||
// Обновляем информацию об устройстве
|
||||
existingDevice.deviceInfo = { ...existingDevice.deviceInfo, ...deviceInfo };
|
||||
existingDevice.capabilities = existingDevice.parseCapabilities(deviceInfo);
|
||||
|
||||
console.log(`Device reconnected: ${deviceId}`);
|
||||
return existingDevice;
|
||||
} else {
|
||||
// Создаем новое устройство
|
||||
const device = new DeviceInfo(deviceId, deviceInfo, socket);
|
||||
this.devices.set(deviceId, device);
|
||||
console.log(`New device registered: ${deviceId}`);
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,9 +45,31 @@ class DeviceManager {
|
||||
* @returns {OperatorInfo}
|
||||
*/
|
||||
registerOperator(operatorId, operatorInfo, socket) {
|
||||
const operator = new OperatorInfo(operatorId, operatorInfo, socket);
|
||||
this.operators.set(operatorId, operator);
|
||||
return operator;
|
||||
// Проверяем, существует ли уже оператор с этим ID
|
||||
const existingOperator = this.operators.get(operatorId);
|
||||
|
||||
if (existingOperator) {
|
||||
// Обновляем существующего оператора при переподключении
|
||||
existingOperator.socket = socket;
|
||||
existingOperator.status = 'connected';
|
||||
existingOperator.connectedAt = Date.now();
|
||||
existingOperator.lastSeen = Date.now();
|
||||
|
||||
// Обновляем информацию об операторе только если она предоставлена
|
||||
if (operatorInfo) {
|
||||
existingOperator.operatorInfo = { ...existingOperator.operatorInfo, ...operatorInfo };
|
||||
existingOperator.permissions = operatorInfo.permissions || existingOperator.permissions;
|
||||
}
|
||||
|
||||
console.log(`Operator reconnected: ${operatorId}`);
|
||||
return existingOperator;
|
||||
} else {
|
||||
// Создаем нового оператора
|
||||
const operator = new OperatorInfo(operatorId, operatorInfo, socket);
|
||||
this.operators.set(operatorId, operator);
|
||||
console.log(`New operator registered: ${operatorId}`);
|
||||
return operator;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +126,8 @@ class DeviceManager {
|
||||
const device = this.devices.get(deviceId);
|
||||
if (device) {
|
||||
device.disconnect();
|
||||
this.devices.delete(deviceId);
|
||||
// НЕ удаляем устройство из Map, только обновляем статус
|
||||
// Устройство может быть переподключено позже
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +139,116 @@ class DeviceManager {
|
||||
const operator = this.operators.get(operatorId);
|
||||
if (operator) {
|
||||
operator.disconnect();
|
||||
this.operators.delete(operatorId);
|
||||
// НЕ удаляем оператора из Map, только обновляем статус
|
||||
// Оператор может быть переподключен позже
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статистики подключений
|
||||
* @returns {object}
|
||||
*/
|
||||
getConnectionStats() {
|
||||
const connectedDevices = this.getConnectedDevices();
|
||||
const connectedOperators = this.getConnectedOperators();
|
||||
|
||||
return {
|
||||
devices: {
|
||||
total: this.devices.size,
|
||||
connected: connectedDevices.length,
|
||||
available: connectedDevices.filter(d => d.canAcceptNewSession()).length,
|
||||
busy: connectedDevices.filter(d => !d.canAcceptNewSession()).length
|
||||
},
|
||||
operators: {
|
||||
total: this.operators.size,
|
||||
connected: connectedOperators.length,
|
||||
active: connectedOperators.filter(o => o.hasActiveSessions()).length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск устройства по сокету
|
||||
* @param {object} socket
|
||||
* @returns {DeviceInfo|null}
|
||||
*/
|
||||
findDeviceBySocket(socket) {
|
||||
for (const device of this.devices.values()) {
|
||||
if (device.socket === socket) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск оператора по сокету
|
||||
* @param {object} socket
|
||||
* @returns {OperatorInfo|null}
|
||||
*/
|
||||
findOperatorBySocket(socket) {
|
||||
for (const operator of this.operators.values()) {
|
||||
if (operator.socket === socket) {
|
||||
return operator;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка отключенных устройств и операторов
|
||||
*/
|
||||
cleanupDisconnected() {
|
||||
// Очищаем отключенные устройства
|
||||
for (const [deviceId, device] of this.devices.entries()) {
|
||||
if (!device.isConnected()) {
|
||||
console.log(`Removing disconnected device: ${deviceId}`);
|
||||
this.devices.delete(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем отключенных операторов
|
||||
for (const [operatorId, operator] of this.operators.entries()) {
|
||||
if (!operator.isConnected()) {
|
||||
console.log(`Removing disconnected operator: ${operatorId}`);
|
||||
this.operators.delete(operatorId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка возможности создания сессии
|
||||
* @param {string} deviceId
|
||||
* @param {string} operatorId
|
||||
* @returns {object} {canConnect: boolean, reason?: string}
|
||||
*/
|
||||
canCreateSession(deviceId, operatorId) {
|
||||
const device = this.getDevice(deviceId);
|
||||
const operator = this.getOperator(operatorId);
|
||||
|
||||
if (!device) {
|
||||
return { canConnect: false, reason: 'Device not found' };
|
||||
}
|
||||
|
||||
if (!operator) {
|
||||
return { canConnect: false, reason: 'Operator not found' };
|
||||
}
|
||||
|
||||
if (!device.isConnected()) {
|
||||
return { canConnect: false, reason: 'Device not connected' };
|
||||
}
|
||||
|
||||
if (!operator.isConnected()) {
|
||||
return { canConnect: false, reason: 'Operator not connected' };
|
||||
}
|
||||
|
||||
if (!device.canAcceptNewSession()) {
|
||||
return { canConnect: false, reason: 'Device busy or unavailable' };
|
||||
}
|
||||
|
||||
return { canConnect: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление местоположения устройства
|
||||
* @param {string} deviceId
|
||||
@@ -226,7 +375,15 @@ class DeviceInfo {
|
||||
* @returns {object}
|
||||
*/
|
||||
parseCapabilities(deviceInfo) {
|
||||
const availableCameras = deviceInfo.availableCameras?.split(',') || ['back'];
|
||||
// Поддерживаем как массив, так и строку для availableCameras
|
||||
let availableCameras;
|
||||
if (Array.isArray(deviceInfo.availableCameras)) {
|
||||
availableCameras = deviceInfo.availableCameras;
|
||||
} else if (typeof deviceInfo.availableCameras === 'string') {
|
||||
availableCameras = deviceInfo.availableCameras.split(',');
|
||||
} else {
|
||||
availableCameras = ['back']; // fallback
|
||||
}
|
||||
|
||||
return {
|
||||
cameras: availableCameras,
|
||||
@@ -243,7 +400,10 @@ class DeviceInfo {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isConnected() {
|
||||
return this.socket && this.socket.connected && this.status === 'connected';
|
||||
// Устройство подключено если есть активный сокет и статус не 'disconnected'
|
||||
return this.socket &&
|
||||
this.socket.connected &&
|
||||
this.status !== 'disconnected';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,7 +526,7 @@ class OperatorInfo {
|
||||
this.lastSeen = Date.now();
|
||||
this.activeSessions = new Set();
|
||||
this.totalSessions = 0;
|
||||
this.permissions = this.operatorInfo.permissions || ['view_cameras', 'request_camera'];
|
||||
this.permissions = this.operatorInfo.permissions || ['view_cameras', 'request_camera', 'initiate_connection'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,6 +563,22 @@ class OperatorInfo {
|
||||
this.activeSessions.delete(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка наличия активных сессий
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasActiveSessions() {
|
||||
return this.activeSessions.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение количества активных сессий
|
||||
* @returns {number}
|
||||
*/
|
||||
getActiveSessionsCount() {
|
||||
return this.activeSessions.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление последнего времени активности
|
||||
*/
|
||||
|
||||
@@ -334,4 +334,192 @@ router.post('/ping/:deviceId', authenticateOperator, requirePermission('view_cam
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/operators/connections/request
|
||||
* Инициация подключения к устройству
|
||||
*/
|
||||
router.post('/connections/request', authenticateOperator, requirePermission('initiate_connection'), async (req, res) => {
|
||||
try {
|
||||
const { deviceId, cameraType = 'back' } = req.body;
|
||||
const { connectionManager } = req.app.locals;
|
||||
|
||||
if (!deviceId) {
|
||||
return res.status(400).json({ error: 'Device ID required' });
|
||||
}
|
||||
|
||||
const result = await connectionManager.initiateConnection(
|
||||
req.operator.operatorId,
|
||||
deviceId,
|
||||
cameraType
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
connectionId: result.connectionId,
|
||||
sessionId: result.sessionId,
|
||||
message: 'Connection request initiated'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/operators/connections/:connectionId/accept
|
||||
* Принятие подключения (используется Android устройством)
|
||||
*/
|
||||
router.put('/connections/:connectionId/accept', async (req, res) => {
|
||||
try {
|
||||
const { connectionId } = req.params;
|
||||
const { connectionManager } = req.app.locals;
|
||||
|
||||
const result = await connectionManager.acceptConnection(connectionId, req.body);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
connection: result,
|
||||
message: 'Connection accepted'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/operators/connections/:connectionId/reject
|
||||
* Отклонение подключения
|
||||
*/
|
||||
router.put('/connections/:connectionId/reject', async (req, res) => {
|
||||
try {
|
||||
const { connectionId } = req.params;
|
||||
const { reason = 'User rejected' } = req.body;
|
||||
const { connectionManager } = req.app.locals;
|
||||
|
||||
const result = await connectionManager.rejectConnection(connectionId, reason);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Connection rejected',
|
||||
reason: reason
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/operators/connections/:connectionId
|
||||
* Завершение активного подключения
|
||||
*/
|
||||
router.delete('/connections/:connectionId', async (req, res) => {
|
||||
try {
|
||||
const { connectionId } = req.params;
|
||||
const { reason = 'Connection terminated by user' } = req.body;
|
||||
const { connectionManager } = req.app.locals;
|
||||
|
||||
const result = await connectionManager.terminateConnection(connectionId, reason);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Connection terminated',
|
||||
reason: reason
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/operators/connections
|
||||
* Получение списка подключений оператора
|
||||
*/
|
||||
router.get('/connections', authenticateOperator, (req, res) => {
|
||||
try {
|
||||
const { connectionManager } = req.app.locals;
|
||||
const connections = connectionManager.getOperatorConnections(req.operator.operatorId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
connections: connections,
|
||||
total: connections.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/operators/connections/:connectionId
|
||||
* Получение информации о конкретном подключении
|
||||
*/
|
||||
router.get('/connections/:connectionId', (req, res) => {
|
||||
try {
|
||||
const { connectionId } = req.params;
|
||||
const { connectionManager } = req.app.locals;
|
||||
|
||||
const connection = connectionManager.getConnection(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
return res.status(404).json({ error: 'Connection not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
connection: connection
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/operators/connections/:connectionId/webrtc
|
||||
* Получение WebRTC информации подключения
|
||||
*/
|
||||
router.get('/connections/:connectionId/webrtc', (req, res) => {
|
||||
try {
|
||||
const { connectionId } = req.params;
|
||||
const { connectionManager } = req.app.locals;
|
||||
|
||||
const webrtcInfo = connectionManager.getWebRTCInfo(connectionId);
|
||||
|
||||
if (!webrtcInfo) {
|
||||
return res.status(404).json({ error: 'WebRTC info not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
webrtc: webrtcInfo
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/operators/stats
|
||||
* Получение статистики подключений
|
||||
*/
|
||||
router.get('/stats', authenticateOperator, (req, res) => {
|
||||
try {
|
||||
const { connectionManager } = req.app.locals;
|
||||
const stats = connectionManager.getConnectionStats();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
stats: stats
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,3 +1,5 @@
|
||||
const path = require('path');
|
||||
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
@@ -8,6 +10,7 @@ const winston = require('winston');
|
||||
// Импорт наших менеджеров
|
||||
const { SessionManager } = require('./managers/SessionManager');
|
||||
const { DeviceManager } = require('./managers/DeviceManager');
|
||||
const { ConnectionManager } = require('./managers/ConnectionManager');
|
||||
|
||||
// Импорт роутов
|
||||
const operatorsRouter = require('./routes/operators');
|
||||
@@ -38,15 +41,41 @@ const io = socketIo(server, {
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static('public')); // Для статических файлов демо
|
||||
app.use(express.static(path.join(__dirname, '../public'))); // Статические файлы
|
||||
|
||||
// Роуты для веб-интерфейсов
|
||||
app.get('/', (req, res) => {
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
||||
|
||||
if (isMobile) {
|
||||
// Перенаправляем мобильные устройства на мобильную версию
|
||||
res.sendFile(path.join(__dirname, '../public/mobile.html'));
|
||||
} else {
|
||||
// Десктопная версия (демо)
|
||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||
}
|
||||
});
|
||||
|
||||
// Принудительная мобильная версия
|
||||
app.get('/mobile', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../public/mobile.html'));
|
||||
});
|
||||
|
||||
// Принудительная десктопная версия
|
||||
app.get('/demo', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||
});
|
||||
|
||||
// Инициализация менеджеров
|
||||
const sessionManager = new SessionManager();
|
||||
const deviceManager = new DeviceManager();
|
||||
const connectionManager = new ConnectionManager(sessionManager, deviceManager, logger);
|
||||
|
||||
// Делаем менеджеры доступными в роутах
|
||||
app.locals.sessionManager = sessionManager;
|
||||
app.locals.deviceManager = deviceManager;
|
||||
app.locals.connectionManager = connectionManager;
|
||||
app.locals.logger = logger;
|
||||
app.locals.io = io;
|
||||
|
||||
@@ -81,7 +110,36 @@ app.get('/api/devices', (req, res) => {
|
||||
|
||||
// WebSocket обработчики
|
||||
io.on('connection', (socket) => {
|
||||
logger.info(`New connection: ${socket.id}`);
|
||||
const userAgent = socket.handshake.headers['user-agent'] || '';
|
||||
const isAndroidClient = userAgent.includes('okhttp');
|
||||
const isMobileWeb = !isAndroidClient && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
||||
|
||||
logger.info(`New connection: ${socket.id}`, {
|
||||
address: socket.handshake.address,
|
||||
userAgent: userAgent,
|
||||
isAndroid: isAndroidClient,
|
||||
isMobileWeb: isMobileWeb
|
||||
});
|
||||
|
||||
if (isAndroidClient) {
|
||||
logger.info(`🤖 Android client connected: ${socket.id}`);
|
||||
// Логируем все события от Android клиента
|
||||
socket.onAny((eventName, ...args) => {
|
||||
logger.info(`📱 Android event: ${eventName}`, args[0]);
|
||||
});
|
||||
|
||||
// Отправляем приветственное сообщение Android клиенту
|
||||
socket.emit('server:hello', {
|
||||
message: 'Server ready for registration',
|
||||
expectedEvent: 'register:android'
|
||||
});
|
||||
} else if (isMobileWeb) {
|
||||
logger.info(`📱 Mobile web client connected: ${socket.id}`);
|
||||
// Логируем события от мобильного веб-клиента
|
||||
socket.onAny((eventName, ...args) => {
|
||||
logger.info(`🌐 Mobile web event: ${eventName}`, args[0]);
|
||||
});
|
||||
}
|
||||
|
||||
// Регистрация Android клиента
|
||||
socket.on('register:android', (data) => {
|
||||
@@ -107,8 +165,46 @@ io.on('connection', (socket) => {
|
||||
socket.emit('register:success', { deviceId });
|
||||
});
|
||||
|
||||
// Регистрация оператора
|
||||
// Регистрация мобильного веб-клиента
|
||||
socket.on('register:mobile_web', (data) => {
|
||||
const { deviceId, deviceInfo } = data;
|
||||
|
||||
// Регистрируем мобильное веб-устройство
|
||||
const device = deviceManager.registerDevice(deviceId, {
|
||||
...deviceInfo,
|
||||
platform: 'mobile_web',
|
||||
type: 'web_camera'
|
||||
}, socket);
|
||||
|
||||
logger.info(`Mobile web client registered: ${deviceId}`, deviceInfo);
|
||||
|
||||
// Уведомляем всех операторов о новом устройстве
|
||||
const operatorSockets = Array.from(deviceManager.operators.values())
|
||||
.filter(op => op.isConnected())
|
||||
.map(op => op.socket);
|
||||
|
||||
operatorSockets.forEach(opSocket => {
|
||||
opSocket.emit('device:connected', {
|
||||
deviceId,
|
||||
deviceInfo: device.getSummary()
|
||||
});
|
||||
});
|
||||
|
||||
socket.emit('register:success', { deviceId });
|
||||
});
|
||||
|
||||
// Fallback: если Android отправляет register:operator вместо register:android
|
||||
socket.on('register:operator', (data) => {
|
||||
const userAgent = socket.handshake.headers['user-agent'] || '';
|
||||
if (userAgent.includes('okhttp')) {
|
||||
logger.warn(`🚨 Android client sent wrong event! ${socket.id} sent 'register:operator' instead of 'register:android'`);
|
||||
socket.emit('register:error', {
|
||||
error: 'Android clients should use register:android event, not register:operator'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Обычная обработка для реальных операторов
|
||||
const { operatorId, operatorInfo } = data;
|
||||
const finalOperatorId = operatorId || uuidv4();
|
||||
|
||||
@@ -127,47 +223,122 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Запрос на подключение к камере (только через WebSocket для обратной совместимости)
|
||||
socket.on('camera:request', (data) => {
|
||||
// Запрос на подключение к камере через ConnectionManager
|
||||
socket.on('camera:request', async (data) => {
|
||||
const { deviceId, cameraType = 'back' } = data;
|
||||
|
||||
logger.info(`📷 Camera request received from operator socket ${socket.id}`);
|
||||
logger.info(`📷 Request data:`, { deviceId, cameraType });
|
||||
|
||||
// Получаем оператора из менеджера устройств
|
||||
const operator = Array.from(deviceManager.operators.values())
|
||||
.find(op => op.socket === socket);
|
||||
|
||||
if (!operator) {
|
||||
logger.error(`❌ Operator not found for socket ${socket.id}`);
|
||||
socket.emit('camera:error', { error: 'Operator not registered' });
|
||||
return;
|
||||
}
|
||||
|
||||
const device = deviceManager.getDevice(deviceId);
|
||||
if (!device || !device.canAcceptNewSession()) {
|
||||
socket.emit('camera:error', { error: 'Device not available' });
|
||||
return;
|
||||
logger.info(`✅ Operator found: ${operator.operatorId}`);
|
||||
|
||||
try {
|
||||
// Используем ConnectionManager для создания подключения
|
||||
const connection = await connectionManager.initiateConnection(
|
||||
operator.operatorId,
|
||||
deviceId,
|
||||
cameraType
|
||||
);
|
||||
|
||||
logger.info(`✅ Connection initiated: ${connection.connectionId}`);
|
||||
|
||||
// Уведомляем оператора о создании подключения
|
||||
socket.emit('connection:initiated', {
|
||||
connectionId: connection.connectionId,
|
||||
sessionId: connection.sessionId,
|
||||
deviceId: deviceId,
|
||||
cameraType: cameraType,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to initiate connection: ${error.message}`);
|
||||
socket.emit('camera:error', { error: error.message });
|
||||
}
|
||||
|
||||
// Создаем сессию
|
||||
const session = sessionManager.createSession(deviceId, operator.operatorId, cameraType);
|
||||
|
||||
logger.info(`Camera request: operator ${operator.operatorId} -> device ${deviceId}`);
|
||||
|
||||
// Отправляем запрос Android клиенту
|
||||
device.socket.emit('camera:request', {
|
||||
sessionId: session.sessionId,
|
||||
operatorId: operator.operatorId,
|
||||
cameraType
|
||||
});
|
||||
|
||||
// Добавляем сессию к участникам
|
||||
device.addSession(session.sessionId);
|
||||
operator.addSession(session.sessionId);
|
||||
|
||||
socket.emit('camera:request-sent', { sessionId: session.sessionId, deviceId });
|
||||
});
|
||||
|
||||
// Ответ от Android клиента на запрос камеры
|
||||
socket.on('camera:response', (data) => {
|
||||
// Ответ от Android клиента на запрос камеры через ConnectionManager
|
||||
socket.on('camera:response', async (data) => {
|
||||
const { sessionId, accepted, streamUrl, error } = data;
|
||||
|
||||
logger.info(`📱 Camera response received from Android: sessionId=${sessionId}, accepted=${accepted}`);
|
||||
|
||||
try {
|
||||
if (accepted) {
|
||||
// Принимаем подключение через ConnectionManager
|
||||
const connection = await connectionManager.acceptConnection(sessionId, { streamUrl });
|
||||
|
||||
logger.info(`✅ Connection accepted: ${connection.connectionId}`);
|
||||
|
||||
// Получаем оператора для уведомления
|
||||
const operator = deviceManager.getOperator(connection.operatorId);
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('connection:accepted', {
|
||||
connectionId: connection.connectionId,
|
||||
sessionId: sessionId,
|
||||
deviceId: connection.deviceId,
|
||||
cameraType: connection.cameraType,
|
||||
streamUrl: streamUrl,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// Отправляем старое событие для обратной совместимости
|
||||
operator.socket.emit('camera:response', {
|
||||
success: true,
|
||||
sessionId: sessionId,
|
||||
session: {
|
||||
id: sessionId,
|
||||
deviceId: connection.deviceId,
|
||||
cameraType: connection.cameraType
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Отклоняем подключение через ConnectionManager
|
||||
await connectionManager.rejectConnection(sessionId, error);
|
||||
|
||||
logger.info(`❌ Connection rejected: sessionId=${sessionId}, error=${error}`);
|
||||
|
||||
// Находим подключение для получения информации об операторе
|
||||
const connection = connectionManager.getConnection(sessionId);
|
||||
if (connection) {
|
||||
const operator = deviceManager.getOperator(connection.operatorId);
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('connection:rejected', {
|
||||
sessionId: sessionId,
|
||||
deviceId: connection.deviceId,
|
||||
cameraType: connection.cameraType,
|
||||
error: error
|
||||
});
|
||||
|
||||
// Отправляем старое событие для обратной совместимости
|
||||
operator.socket.emit('camera:response', {
|
||||
success: false,
|
||||
sessionId: sessionId,
|
||||
message: error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to handle camera response: ${error.message}`);
|
||||
socket.emit('camera:error', { error: error.message });
|
||||
}
|
||||
|
||||
// Переключение камеры в активной сессии
|
||||
socket.on('camera:switch', (data) => {
|
||||
const { sessionId, cameraType } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
@@ -175,27 +346,76 @@ io.on('connection', (socket) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем участников сессии
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
if (!operator || !operator.isConnected()) {
|
||||
socket.emit('camera:error', { error: 'Operator not found' });
|
||||
|
||||
// Проверяем, что запрос идет от оператора этой сессии
|
||||
if (!operator || operator.socket !== socket) {
|
||||
socket.emit('camera:error', { error: 'Unauthorized to switch camera in this session' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (accepted) {
|
||||
session.updateStatus('active', { streamUrl });
|
||||
logger.info(`Camera stream started: session ${sessionId}`);
|
||||
operator.socket.emit('camera:stream-ready', { sessionId, streamUrl });
|
||||
} else {
|
||||
session.updateStatus('denied', { error });
|
||||
logger.info(`Camera request denied: session ${sessionId}`, error);
|
||||
operator.socket.emit('camera:denied', { sessionId, error });
|
||||
|
||||
// Очищаем отклоненную сессию
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
if (device) device.removeSession(sessionId);
|
||||
operator.removeSession(sessionId);
|
||||
sessionManager.closeSession(sessionId);
|
||||
// Проверяем, что сессия активна
|
||||
if (session.status !== 'active') {
|
||||
socket.emit('camera:error', { error: 'Session is not active' });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Camera switch requested: session ${sessionId}, camera ${cameraType}`);
|
||||
|
||||
// Отправляем запрос на переключение устройству
|
||||
if (device && device.isConnected()) {
|
||||
device.socket.emit('camera:switch', {
|
||||
sessionId: sessionId,
|
||||
cameraType: cameraType
|
||||
});
|
||||
|
||||
// Обновляем тип камеры в сессии
|
||||
session.cameraType = cameraType;
|
||||
} else {
|
||||
socket.emit('camera:error', { error: 'Device not connected' });
|
||||
}
|
||||
});
|
||||
|
||||
// Завершение сессии по инициативе оператора
|
||||
socket.on('session:end', (data) => {
|
||||
const { sessionId } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
socket.emit('session:error', { error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем участников сессии
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Проверяем, что запрос идет от оператора этой сессии
|
||||
if (!operator || operator.socket !== socket) {
|
||||
socket.emit('session:error', { error: 'Unauthorized to end this session' });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Session ended by operator: ${sessionId}`);
|
||||
|
||||
// Уведомляем устройство о завершении
|
||||
if (device && device.isConnected()) {
|
||||
device.socket.emit('camera:disconnect', { sessionId });
|
||||
device.removeSession(sessionId);
|
||||
}
|
||||
|
||||
// Уведомляем оператора о завершении
|
||||
operator.socket.emit('session:ended', {
|
||||
sessionId: sessionId,
|
||||
deviceId: session.deviceId,
|
||||
reason: 'Ended by operator'
|
||||
});
|
||||
operator.removeSession(sessionId);
|
||||
|
||||
// Закрываем сессию
|
||||
sessionManager.closeSession(sessionId);
|
||||
});
|
||||
|
||||
// WebRTC сигнализация
|
||||
@@ -212,11 +432,15 @@ io.on('connection', (socket) => {
|
||||
if (device && device.socket === socket && operator && operator.isConnected()) {
|
||||
operator.socket.emit('webrtc:offer', { sessionId, offer });
|
||||
session.updateWebRTCState('offer_sent');
|
||||
// Обновляем состояние в ConnectionManager
|
||||
connectionManager.updateWebRTCState(sessionId, 'offer_sent');
|
||||
}
|
||||
// Если отправитель - оператор, то получатель - устройство
|
||||
else if (operator && operator.socket === socket && device && device.isConnected()) {
|
||||
device.socket.emit('webrtc:offer', { sessionId, offer });
|
||||
session.updateWebRTCState('offer_sent');
|
||||
// Обновляем состояние в ConnectionManager
|
||||
connectionManager.updateWebRTCState(sessionId, 'offer_sent');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -233,11 +457,15 @@ io.on('connection', (socket) => {
|
||||
if (device && device.socket === socket && operator && operator.isConnected()) {
|
||||
operator.socket.emit('webrtc:answer', { sessionId, answer });
|
||||
session.updateWebRTCState('answer_sent');
|
||||
// Обновляем состояние в ConnectionManager
|
||||
connectionManager.updateWebRTCState(sessionId, 'answer_sent');
|
||||
}
|
||||
// Если отправитель - оператор, то получатель - устройство
|
||||
else if (operator && operator.socket === socket && device && device.isConnected()) {
|
||||
device.socket.emit('webrtc:answer', { sessionId, answer });
|
||||
session.updateWebRTCState('answer_sent');
|
||||
// Обновляем состояние в ConnectionManager
|
||||
connectionManager.updateWebRTCState(sessionId, 'answer_sent');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -259,6 +487,38 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик установки WebRTC соединения
|
||||
socket.on('webrtc:connected', (data) => {
|
||||
const { sessionId } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
session.updateWebRTCState('connected');
|
||||
// Обновляем состояние в ConnectionManager
|
||||
connectionManager.updateWebRTCState(sessionId, 'connected');
|
||||
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Уведомляем обе стороны о переходе в прямой режим
|
||||
if (device && device.isConnected()) {
|
||||
device.socket.emit('webrtc:direct-mode', {
|
||||
sessionId,
|
||||
message: 'WebRTC connection established, switching to direct mode'
|
||||
});
|
||||
}
|
||||
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('webrtc:direct-mode', {
|
||||
sessionId,
|
||||
message: 'WebRTC connection established, switching to direct mode'
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`🔗 WebRTC connection established: ${sessionId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Переключение типа камеры
|
||||
socket.on('camera:switch', (data) => {
|
||||
const { sessionId, cameraType } = data;
|
||||
@@ -294,6 +554,11 @@ io.on('connection', (socket) => {
|
||||
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('camera:disconnected', { sessionId });
|
||||
operator.socket.emit('session:ended', {
|
||||
sessionId: sessionId,
|
||||
deviceId: session.deviceId,
|
||||
reason: 'Device disconnected'
|
||||
});
|
||||
operator.removeSession(sessionId);
|
||||
}
|
||||
|
||||
@@ -309,8 +574,81 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка отключения
|
||||
socket.on('disconnect', (reason) => {
|
||||
// Новые события для ConnectionManager
|
||||
|
||||
// Завершение подключения от оператора
|
||||
socket.on('connection:terminate', async (data) => {
|
||||
const { connectionId } = data;
|
||||
|
||||
logger.info(`🔚 Connection termination requested: ${connectionId}`);
|
||||
|
||||
try {
|
||||
await connectionManager.terminateConnection(connectionId);
|
||||
|
||||
socket.emit('connection:terminated', {
|
||||
connectionId: connectionId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
logger.info(`✅ Connection terminated: ${connectionId}`);
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to terminate connection: ${error.message}`);
|
||||
socket.emit('connection:error', { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Запрос статистики подключений
|
||||
socket.on('connection:status', (data, callback) => {
|
||||
const stats = connectionManager.getConnectionStats();
|
||||
|
||||
if (callback) {
|
||||
callback({
|
||||
success: true,
|
||||
stats: stats,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
socket.emit('connection:status_response', {
|
||||
stats: stats,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Список активных подключений для оператора
|
||||
socket.on('connection:list', (data, callback) => {
|
||||
const operator = Array.from(deviceManager.operators.values())
|
||||
.find(op => op.socket === socket);
|
||||
|
||||
if (!operator) {
|
||||
const error = 'Operator not found';
|
||||
if (callback) {
|
||||
callback({ success: false, error });
|
||||
} else {
|
||||
socket.emit('connection:error', { error });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = connectionManager.getOperatorConnections(operator.operatorId);
|
||||
|
||||
if (callback) {
|
||||
callback({
|
||||
success: true,
|
||||
connections: connections,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
socket.emit('connection:list_response', {
|
||||
connections: connections,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка отключения с ConnectionManager
|
||||
socket.on('disconnect', async (reason) => {
|
||||
logger.info(`Client disconnected: ${socket.id}, reason: ${reason}`);
|
||||
|
||||
// Находим устройство или оператора по сокету
|
||||
@@ -321,6 +659,9 @@ io.on('connection', (socket) => {
|
||||
.find(op => op.socket === socket);
|
||||
|
||||
if (device) {
|
||||
// Очищаем подключения устройства через ConnectionManager
|
||||
await connectionManager.cleanupDeviceConnections(device.deviceId);
|
||||
|
||||
// Уведомляем операторов об отключении устройства
|
||||
const operators = deviceManager.getConnectedOperators();
|
||||
operators.forEach(op => {
|
||||
@@ -335,6 +676,9 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
|
||||
if (operator) {
|
||||
// Очищаем подключения оператора через ConnectionManager
|
||||
await connectionManager.cleanupOperatorConnections(operator.operatorId);
|
||||
|
||||
// Завершаем активные сессии оператора
|
||||
sessionManager.closeOperatorSessions(operator.operatorId);
|
||||
deviceManager.disconnectOperator(operator.operatorId);
|
||||
@@ -370,12 +714,15 @@ process.on('SIGINT', () => {
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const HOST = process.env.HOST || '0.0.0.0'; // Слушаем на всех интерфейсах для доступа из эмулятора
|
||||
|
||||
server.listen(PORT, () => {
|
||||
logger.info(`GodEye Backend Server running on port ${PORT}`);
|
||||
console.log(`🚀 Server started on http://localhost:${PORT}`);
|
||||
server.listen(PORT, HOST, () => {
|
||||
logger.info(`GodEye Backend Server running on ${HOST}:${PORT}`);
|
||||
console.log(`🚀 Server started on http://${HOST}:${PORT}`);
|
||||
console.log(`📊 Admin API: http://localhost:${PORT}/api/admin/stats`);
|
||||
console.log(`👥 Operators API: http://localhost:${PORT}/api/operators/devices`);
|
||||
console.log(`📱 Status: http://localhost:${PORT}/api/status`);
|
||||
console.log(`🌐 Demo: http://localhost:${PORT}/`);
|
||||
});
|
||||
console.log(`📱 Android emulator: http://10.0.2.2:${PORT}/`);
|
||||
});
|
||||
|
||||
|
||||
539
backend/test_chunk.js
Normal file
539
backend/test_chunk.js
Normal file
@@ -0,0 +1,539 @@
|
||||
io.on('connection', (socket) => {
|
||||
const userAgent = socket.handshake.headers['user-agent'] || '';
|
||||
const isAndroidClient = userAgent.includes('okhttp');
|
||||
const isMobileWeb = !isAndroidClient && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
||||
|
||||
logger.info(`New connection: ${socket.id}`, {
|
||||
address: socket.handshake.address,
|
||||
userAgent: userAgent,
|
||||
isAndroid: isAndroidClient,
|
||||
isMobileWeb: isMobileWeb
|
||||
});
|
||||
|
||||
if (isAndroidClient) {
|
||||
logger.info(`🤖 Android client connected: ${socket.id}`);
|
||||
// Логируем все события от Android клиента
|
||||
socket.onAny((eventName, ...args) => {
|
||||
logger.info(`📱 Android event: ${eventName}`, args[0]);
|
||||
});
|
||||
|
||||
// Отправляем приветственное сообщение Android клиенту
|
||||
socket.emit('server:hello', {
|
||||
message: 'Server ready for registration',
|
||||
expectedEvent: 'register:android'
|
||||
});
|
||||
} else if (isMobileWeb) {
|
||||
logger.info(`📱 Mobile web client connected: ${socket.id}`);
|
||||
// Логируем события от мобильного веб-клиента
|
||||
socket.onAny((eventName, ...args) => {
|
||||
logger.info(`🌐 Mobile web event: ${eventName}`, args[0]);
|
||||
});
|
||||
}
|
||||
|
||||
// Регистрация Android клиента
|
||||
socket.on('register:android', (data) => {
|
||||
const { deviceId, deviceInfo } = data;
|
||||
|
||||
// Регистрируем устройство через DeviceManager
|
||||
const device = deviceManager.registerDevice(deviceId, deviceInfo, socket);
|
||||
|
||||
logger.info(`Android client registered: ${deviceId}`, deviceInfo);
|
||||
|
||||
// Уведомляем всех операторов о новом устройстве
|
||||
const operatorSockets = Array.from(deviceManager.operators.values())
|
||||
.filter(op => op.isConnected())
|
||||
.map(op => op.socket);
|
||||
|
||||
operatorSockets.forEach(opSocket => {
|
||||
opSocket.emit('device:connected', {
|
||||
deviceId,
|
||||
deviceInfo: device.getSummary()
|
||||
});
|
||||
});
|
||||
|
||||
socket.emit('register:success', { deviceId });
|
||||
});
|
||||
|
||||
// Регистрация мобильного веб-клиента
|
||||
socket.on('register:mobile_web', (data) => {
|
||||
const { deviceId, deviceInfo } = data;
|
||||
|
||||
// Регистрируем мобильное веб-устройство
|
||||
const device = deviceManager.registerDevice(deviceId, {
|
||||
...deviceInfo,
|
||||
platform: 'mobile_web',
|
||||
type: 'web_camera'
|
||||
}, socket);
|
||||
|
||||
logger.info(`Mobile web client registered: ${deviceId}`, deviceInfo);
|
||||
|
||||
// Уведомляем всех операторов о новом устройстве
|
||||
const operatorSockets = Array.from(deviceManager.operators.values())
|
||||
.filter(op => op.isConnected())
|
||||
.map(op => op.socket);
|
||||
|
||||
operatorSockets.forEach(opSocket => {
|
||||
opSocket.emit('device:connected', {
|
||||
deviceId,
|
||||
deviceInfo: device.getSummary()
|
||||
});
|
||||
});
|
||||
|
||||
socket.emit('register:success', { deviceId });
|
||||
});
|
||||
|
||||
// Fallback: если Android отправляет register:operator вместо register:android
|
||||
socket.on('register:operator', (data) => {
|
||||
const userAgent = socket.handshake.headers['user-agent'] || '';
|
||||
if (userAgent.includes('okhttp')) {
|
||||
logger.warn(`🚨 Android client sent wrong event! ${socket.id} sent 'register:operator' instead of 'register:android'`);
|
||||
socket.emit('register:error', {
|
||||
error: 'Android clients should use register:android event, not register:operator'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Обычная обработка для реальных операторов
|
||||
const { operatorId, operatorInfo } = data;
|
||||
const finalOperatorId = operatorId || uuidv4();
|
||||
|
||||
// Регистрируем оператора через DeviceManager
|
||||
const operator = deviceManager.registerOperator(finalOperatorId, operatorInfo, socket);
|
||||
|
||||
logger.info(`Operator registered: ${finalOperatorId}`);
|
||||
|
||||
// Отправляем список доступных устройств
|
||||
const availableDevices = deviceManager.getAvailableDevicesForOperator(finalOperatorId);
|
||||
const devicesData = availableDevices.map(device => device.getSummary());
|
||||
|
||||
socket.emit('register:success', {
|
||||
operatorId: finalOperatorId,
|
||||
availableDevices: devicesData
|
||||
});
|
||||
});
|
||||
|
||||
// Запрос на подключение к камере через ConnectionManager
|
||||
socket.on('camera:request', async (data) => {
|
||||
const { deviceId, cameraType = 'back' } = data;
|
||||
|
||||
logger.info(`📷 Camera request received from operator socket ${socket.id}`);
|
||||
logger.info(`📷 Request data:`, { deviceId, cameraType });
|
||||
|
||||
// Получаем оператора из менеджера устройств
|
||||
const operator = Array.from(deviceManager.operators.values())
|
||||
.find(op => op.socket === socket);
|
||||
|
||||
if (!operator) {
|
||||
logger.error(`❌ Operator not found for socket ${socket.id}`);
|
||||
socket.emit('camera:error', { error: 'Operator not registered' });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`✅ Operator found: ${operator.operatorId}`);
|
||||
|
||||
try {
|
||||
// Используем ConnectionManager для создания подключения
|
||||
const connection = await connectionManager.initiateConnection(
|
||||
operator.operatorId,
|
||||
deviceId,
|
||||
cameraType
|
||||
);
|
||||
|
||||
logger.info(`✅ Connection initiated: ${connection.connectionId}`);
|
||||
|
||||
// Уведомляем оператора о создании подключения
|
||||
socket.emit('connection:initiated', {
|
||||
connectionId: connection.connectionId,
|
||||
sessionId: connection.sessionId,
|
||||
deviceId: deviceId,
|
||||
cameraType: cameraType,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to initiate connection: ${error.message}`);
|
||||
socket.emit('camera:error', { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Ответ от Android клиента на запрос камеры через ConnectionManager
|
||||
socket.on('camera:response', async (data) => {
|
||||
const { sessionId, accepted, streamUrl, error } = data;
|
||||
|
||||
logger.info(`📱 Camera response received from Android: sessionId=${sessionId}, accepted=${accepted}`);
|
||||
|
||||
try {
|
||||
if (accepted) {
|
||||
// Принимаем подключение через ConnectionManager
|
||||
const connection = await connectionManager.acceptConnection(sessionId, { streamUrl });
|
||||
|
||||
logger.info(`✅ Connection accepted: ${connection.connectionId}`);
|
||||
|
||||
// Получаем оператора для уведомления
|
||||
const operator = deviceManager.getOperator(connection.operatorId);
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('connection:accepted', {
|
||||
connectionId: connection.connectionId,
|
||||
sessionId: sessionId,
|
||||
deviceId: connection.deviceId,
|
||||
cameraType: connection.cameraType,
|
||||
streamUrl: streamUrl,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// Отправляем старое событие для обратной совместимости
|
||||
operator.socket.emit('camera:response', {
|
||||
success: true,
|
||||
sessionId: sessionId,
|
||||
session: {
|
||||
id: sessionId,
|
||||
deviceId: connection.deviceId,
|
||||
cameraType: connection.cameraType
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Отклоняем подключение через ConnectionManager
|
||||
await connectionManager.rejectConnection(sessionId, error);
|
||||
|
||||
logger.info(`❌ Connection rejected: sessionId=${sessionId}, error=${error}`);
|
||||
|
||||
// Находим подключение для получения информации об операторе
|
||||
const connection = connectionManager.getConnection(sessionId);
|
||||
if (connection) {
|
||||
const operator = deviceManager.getOperator(connection.operatorId);
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('connection:rejected', {
|
||||
sessionId: sessionId,
|
||||
deviceId: connection.deviceId,
|
||||
cameraType: connection.cameraType,
|
||||
error: error
|
||||
});
|
||||
|
||||
// Отправляем старое событие для обратной совместимости
|
||||
operator.socket.emit('camera:response', {
|
||||
success: false,
|
||||
sessionId: sessionId,
|
||||
message: error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to handle camera response: ${error.message}`);
|
||||
socket.emit('camera:error', { error: error.message });
|
||||
}
|
||||
|
||||
// Переключение камеры в активной сессии
|
||||
socket.on('camera:switch', (data) => {
|
||||
const { sessionId, cameraType } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
socket.emit('camera:error', { error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем участников сессии
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Проверяем, что запрос идет от оператора этой сессии
|
||||
if (!operator || operator.socket !== socket) {
|
||||
socket.emit('camera:error', { error: 'Unauthorized to switch camera in this session' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что сессия активна
|
||||
if (session.status !== 'active') {
|
||||
socket.emit('camera:error', { error: 'Session is not active' });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Camera switch requested: session ${sessionId}, camera ${cameraType}`);
|
||||
|
||||
// Отправляем запрос на переключение устройству
|
||||
if (device && device.isConnected()) {
|
||||
device.socket.emit('camera:switch', {
|
||||
sessionId: sessionId,
|
||||
cameraType: cameraType
|
||||
});
|
||||
|
||||
// Обновляем тип камеры в сессии
|
||||
session.cameraType = cameraType;
|
||||
} else {
|
||||
socket.emit('camera:error', { error: 'Device not connected' });
|
||||
}
|
||||
});
|
||||
|
||||
// Завершение сессии по инициативе оператора
|
||||
socket.on('session:end', (data) => {
|
||||
const { sessionId } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
socket.emit('session:error', { error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем участников сессии
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Проверяем, что запрос идет от оператора этой сессии
|
||||
if (!operator || operator.socket !== socket) {
|
||||
socket.emit('session:error', { error: 'Unauthorized to end this session' });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Session ended by operator: ${sessionId}`);
|
||||
|
||||
// Уведомляем устройство о завершении
|
||||
if (device && device.isConnected()) {
|
||||
device.socket.emit('camera:disconnect', { sessionId });
|
||||
device.removeSession(sessionId);
|
||||
}
|
||||
|
||||
// Уведомляем оператора о завершении
|
||||
operator.socket.emit('session:ended', {
|
||||
sessionId: sessionId,
|
||||
deviceId: session.deviceId,
|
||||
reason: 'Ended by operator'
|
||||
});
|
||||
operator.removeSession(sessionId);
|
||||
|
||||
// Закрываем сессию
|
||||
sessionManager.closeSession(sessionId);
|
||||
});
|
||||
|
||||
// WebRTC сигнализация
|
||||
socket.on('webrtc:offer', (data) => {
|
||||
const { sessionId, offer } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
// Определяем получателя (Android -> Operator или Operator -> Android)
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Если отправитель - устройство, то получатель - оператор
|
||||
if (device && device.socket === socket && operator && operator.isConnected()) {
|
||||
operator.socket.emit('webrtc:offer', { sessionId, offer });
|
||||
session.updateWebRTCState('offer_sent');
|
||||
}
|
||||
// Если отправитель - оператор, то получатель - устройство
|
||||
else if (operator && operator.socket === socket && device && device.isConnected()) {
|
||||
device.socket.emit('webrtc:offer', { sessionId, offer });
|
||||
session.updateWebRTCState('offer_sent');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('webrtc:answer', (data) => {
|
||||
const { sessionId, answer } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Если отправитель - устройство, то получатель - оператор
|
||||
if (device && device.socket === socket && operator && operator.isConnected()) {
|
||||
operator.socket.emit('webrtc:answer', { sessionId, answer });
|
||||
session.updateWebRTCState('answer_sent');
|
||||
}
|
||||
// Если отправитель - оператор, то получатель - устройство
|
||||
else if (operator && operator.socket === socket && device && device.isConnected()) {
|
||||
device.socket.emit('webrtc:answer', { sessionId, answer });
|
||||
session.updateWebRTCState('answer_sent');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('webrtc:ice-candidate', (data) => {
|
||||
const { sessionId, candidate } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Пересылаем ICE кандидата другой стороне
|
||||
if (device && device.socket === socket && operator && operator.isConnected()) {
|
||||
operator.socket.emit('webrtc:ice-candidate', { sessionId, candidate });
|
||||
} else if (operator && operator.socket === socket && device && device.isConnected()) {
|
||||
device.socket.emit('webrtc:ice-candidate', { sessionId, candidate });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Переключение типа камеры
|
||||
socket.on('camera:switch', (data) => {
|
||||
const { sessionId, cameraType } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Проверяем права доступа
|
||||
if (operator && operator.socket === socket && device && device.isConnected()) {
|
||||
device.socket.emit('camera:switch', { sessionId, cameraType });
|
||||
session.switchCamera(cameraType);
|
||||
logger.info(`Camera switch requested: ${sessionId} -> ${cameraType}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Завершение сессии
|
||||
socket.on('camera:disconnect', (data) => {
|
||||
const { sessionId } = data;
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
const device = deviceManager.getDevice(session.deviceId);
|
||||
const operator = deviceManager.getOperator(session.operatorId);
|
||||
|
||||
// Уведомляем участников
|
||||
if (device && device.isConnected()) {
|
||||
device.socket.emit('camera:disconnect', { sessionId });
|
||||
device.removeSession(sessionId);
|
||||
}
|
||||
|
||||
if (operator && operator.isConnected()) {
|
||||
operator.socket.emit('camera:disconnected', { sessionId });
|
||||
operator.socket.emit('session:ended', {
|
||||
sessionId: sessionId,
|
||||
deviceId: session.deviceId,
|
||||
reason: 'Device disconnected'
|
||||
});
|
||||
operator.removeSession(sessionId);
|
||||
}
|
||||
|
||||
sessionManager.closeSession(sessionId);
|
||||
logger.info(`Camera session ended: ${sessionId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Ping-Pong для проверки соединения
|
||||
socket.on('ping', (data, callback) => {
|
||||
if (callback) {
|
||||
callback({ timestamp: Date.now(), ...data });
|
||||
}
|
||||
});
|
||||
|
||||
// Новые события для ConnectionManager
|
||||
|
||||
// Завершение подключения от оператора
|
||||
socket.on('connection:terminate', async (data) => {
|
||||
const { connectionId } = data;
|
||||
|
||||
logger.info(`🔚 Connection termination requested: ${connectionId}`);
|
||||
|
||||
try {
|
||||
await connectionManager.terminateConnection(connectionId);
|
||||
|
||||
socket.emit('connection:terminated', {
|
||||
connectionId: connectionId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
logger.info(`✅ Connection terminated: ${connectionId}`);
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to terminate connection: ${error.message}`);
|
||||
socket.emit('connection:error', { error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Запрос статистики подключений
|
||||
socket.on('connection:status', (data, callback) => {
|
||||
const stats = connectionManager.getConnectionStats();
|
||||
|
||||
if (callback) {
|
||||
callback({
|
||||
success: true,
|
||||
stats: stats,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
socket.emit('connection:status_response', {
|
||||
stats: stats,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Список активных подключений для оператора
|
||||
socket.on('connection:list', (data, callback) => {
|
||||
const operator = Array.from(deviceManager.operators.values())
|
||||
.find(op => op.socket === socket);
|
||||
|
||||
if (!operator) {
|
||||
const error = 'Operator not found';
|
||||
if (callback) {
|
||||
callback({ success: false, error });
|
||||
} else {
|
||||
socket.emit('connection:error', { error });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = connectionManager.getOperatorConnections(operator.operatorId);
|
||||
|
||||
if (callback) {
|
||||
callback({
|
||||
success: true,
|
||||
connections: connections,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
socket.emit('connection:list_response', {
|
||||
connections: connections,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка отключения с ConnectionManager
|
||||
socket.on('disconnect', (reason) => {
|
||||
logger.info(`Client disconnected: ${socket.id}, reason: ${reason}`);
|
||||
|
||||
// Находим устройство или оператора по сокету
|
||||
const device = Array.from(deviceManager.devices.values())
|
||||
.find(d => d.socket === socket);
|
||||
|
||||
const operator = Array.from(deviceManager.operators.values())
|
||||
.find(op => op.socket === socket);
|
||||
|
||||
if (device) {
|
||||
// Очищаем подключения устройства через ConnectionManager
|
||||
connectionManager.cleanupDeviceConnections(device.deviceId);
|
||||
|
||||
// Уведомляем операторов об отключении устройства
|
||||
const operators = deviceManager.getConnectedOperators();
|
||||
operators.forEach(op => {
|
||||
op.socket.emit('device:disconnected', {
|
||||
deviceId: device.deviceId
|
||||
});
|
||||
});
|
||||
|
||||
// Завершаем активные сессии устройства
|
||||
sessionManager.closeDeviceSessions(device.deviceId);
|
||||
deviceManager.disconnectDevice(device.deviceId);
|
||||
}
|
||||
|
||||
if (operator) {
|
||||
// Очищаем подключения оператора через ConnectionManager
|
||||
connectionManager.cleanupOperatorConnections(operator.operatorId);
|
||||
|
||||
// Завершаем активные сессии оператора
|
||||
sessionManager.closeOperatorSessions(operator.operatorId);
|
||||
deviceManager.disconnectOperator(operator.operatorId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Периодическая очистка старых сессий и устройств
|
||||
setInterval(() => {
|
||||
try {
|
||||
Reference in New Issue
Block a user