main commit
This commit is contained in:
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 };
|
||||
Reference in New Issue
Block a user