main commit

This commit is contained in:
2025-10-04 11:55:55 +09:00
parent c8c3274527
commit 4ceccae6ce
678 changed files with 95975 additions and 185 deletions

View 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 };