540 lines
20 KiB
JavaScript
540 lines
20 KiB
JavaScript
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 {
|