# Протокол запросов от сервера к Android устройству ## Обзор Данный документ описывает протокол WebSocket событий, которые сервер отправляет на Android устройство для запроса подтверждения подключения оператора и открытия сеанса камеры. ## Схема работы 1. **Оператор инициирует подключение** через REST API или WebSocket 2. **Сервер создает соединение** в ConnectionManager 3. **Сервер отправляет запрос на Android** через WebSocket 4. **Android отображает диалог** пользователю 5. **Пользователь принимает/отклоняет** запрос 6. **Android отправляет ответ** серверу 7. **Сервер уведомляет оператора** о результате ## События от сервера к Android ### 1. `connection:request` - Запрос на подключение Отправляется Android устройству когда оператор запрашивает доступ к камере. ```javascript // Сервер → Android socket.emit('connection:request', { connectionId: 'uuid-connection-id', sessionId: 'uuid-session-id', operatorId: 'uuid-operator-id', operatorInfo: { name: 'Имя оператора', organization: 'Организация', reason: 'Причина запроса доступа' }, cameraType: 'back', // 'back', 'front', 'wide', 'telephoto' timestamp: '2025-10-04T12:00:00.000Z', expiresAt: '2025-10-04T12:05:00.000Z' // Время истечения запроса (5 минут) }); ``` **Ожидаемый ответ от Android:** - `connection:accept` - пользователь принял запрос - `connection:reject` - пользователь отклонил запрос - Timeout через 5 минут если нет ответа ### 2. `camera:request` - Запрос доступа к камере (Legacy) Для совместимости со старой системой. Используется при прямом запросе камеры. ```javascript // Сервер → Android socket.emit('camera:request', { sessionId: 'uuid-session-id', operatorId: 'uuid-operator-id', cameraType: 'back', timestamp: '2025-10-04T12:00:00.000Z' }); ``` ### 3. `camera:switch` - Переключение камеры Запрос на переключение камеры во время активного сеанса. ```javascript // Сервер → Android socket.emit('camera:switch', { sessionId: 'uuid-session-id', cameraType: 'front', // Новый тип камеры timestamp: '2025-10-04T12:00:00.000Z' }); ``` ### 4. `camera:disconnect` - Завершение сеанса Уведомление о завершении сеанса камеры. ```javascript // Сервер → Android socket.emit('camera:disconnect', { sessionId: 'uuid-session-id', reason: 'operator_disconnect', // 'operator_disconnect', 'timeout', 'error' timestamp: '2025-10-04T12:00:00.000Z' }); ``` ## События от Android к серверу ### 1. `connection:accept` - Принятие подключения ```javascript // Android → Сервер socket.emit('connection:accept', { connectionId: 'uuid-connection-id', sessionId: 'uuid-session-id', cameraType: 'back', webrtcInfo: { supported: true, codecs: ['H264', 'VP8'], resolutions: ['720p', '1080p'] }, timestamp: '2025-10-04T12:00:00.000Z' }); ``` ### 2. `connection:reject` - Отклонение подключения ```javascript // Android → Сервер socket.emit('connection:reject', { connectionId: 'uuid-connection-id', reason: 'user_denied', // 'user_denied', 'camera_busy', 'permission_denied' timestamp: '2025-10-04T12:00:00.000Z' }); ``` ### 3. `camera:response` - Ответ на запрос камеры (Legacy) ```javascript // Android → Сервер socket.emit('camera:response', { sessionId: 'uuid-session-id', accepted: true, // true/false reason: 'camera_granted', // или причина отказа cameraType: 'back', timestamp: '2025-10-04T12:00:00.000Z' }); ``` ## Жизненный цикл подключения ### Успешное подключение ```mermaid sequenceDiagram participant O as Оператор participant S as Сервер participant A as Android O->>S: POST /api/operators/connections/request S->>S: Создание connection в ConnectionManager S->>A: connection:request A->>A: Показ диалога пользователю A->>S: connection:accept S->>S: Обновление connection (status: accepted) S->>O: connection:accepted (WebSocket) Note over O,A: Начало WebRTC сеанса ``` ### Отклонение подключения ```mermaid sequenceDiagram participant O as Оператор participant S as Сервер participant A as Android O->>S: POST /api/operators/connections/request S->>S: Создание connection в ConnectionManager S->>A: connection:request A->>A: Показ диалога пользователю A->>S: connection:reject S->>S: Обновление connection (status: rejected) S->>O: connection:rejected (WebSocket) ``` ### Таймаут подключения ```mermaid sequenceDiagram participant O as Оператор participant S as Сервер participant A as Android O->>S: POST /api/operators/connections/request S->>S: Создание connection в ConnectionManager S->>A: connection:request Note over A: Пользователь не отвечает S->>S: Timeout через 5 минут S->>S: Обновление connection (status: timeout) S->>O: connection:timeout (WebSocket) S->>A: connection:timeout (уведомление) ``` ## Обработка в ConnectionManager ### Инициация подключения ```javascript // В файле /backend/src/managers/ConnectionManager.js async initiateConnection(operatorId, deviceId, cameraType = 'back') { // 1. Создание connection объекта const connection = { connectionId: uuidv4(), sessionId: uuidv4(), operatorId, deviceId, cameraType, status: 'pending', createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString() }; // 2. Сохранение в память this.connections.set(connection.connectionId, connection); // 3. Отправка запроса на Android const device = this.deviceManager.getDevice(deviceId); device.socket.emit('connection:request', { connectionId: connection.connectionId, sessionId: connection.sessionId, operatorId, operatorInfo: operator.operatorInfo, cameraType, timestamp: connection.createdAt, expiresAt: connection.expiresAt }); // 4. Установка таймаута setTimeout(() => { if (connection.status === 'pending') { this.handleConnectionTimeout(connection.connectionId); } }, 5 * 60 * 1000); return connection; } ``` ### Принятие подключения ```javascript async acceptConnection(connectionId, responseData) { const connection = this.connections.get(connectionId); // Обновление статуса connection.status = 'accepted'; connection.acceptedAt = new Date().toISOString(); connection.webrtcInfo = responseData.webrtcInfo; // Создание сессии const session = this.sessionManager.createSession( connection.deviceId, connection.operatorId, connection.cameraType ); // Уведомление оператора const operator = this.deviceManager.getOperator(connection.operatorId); operator.socket.emit('connection:accepted', { connectionId, sessionId: session.sessionId, webrtcInfo: connection.webrtcInfo }); return connection; } ``` ## Обработка в Android приложении ### Регистрация обработчиков событий ```kotlin // В Android клиенте socket.on("connection:request") { args -> val data = args[0] as JSONObject val connectionId = data.getString("connectionId") val operatorInfo = data.getJSONObject("operatorInfo") val cameraType = data.getString("cameraType") // Показ диалога пользователю showConnectionRequestDialog(connectionId, operatorInfo, cameraType) } socket.on("camera:request") { args -> val data = args[0] as JSONObject val sessionId = data.getString("sessionId") val operatorId = data.getString("operatorId") val cameraType = data.getString("cameraType") // Legacy обработка для совместимости handleCameraRequest(sessionId, operatorId, cameraType) } ``` ### Ответ на запрос подключения ```kotlin private fun acceptConnection(connectionId: String, cameraType: String) { val response = JSONObject().apply { put("connectionId", connectionId) put("sessionId", sessionId) put("cameraType", cameraType) put("webrtcInfo", JSONObject().apply { put("supported", true) put("codecs", JSONArray(listOf("H264", "VP8"))) put("resolutions", JSONArray(listOf("720p", "1080p"))) }) put("timestamp", Instant.now().toString()) } socket.emit("connection:accept", response) // Начало подготовки камеры startCameraPreview(cameraType) } private fun rejectConnection(connectionId: String, reason: String) { val response = JSONObject().apply { put("connectionId", connectionId) put("reason", reason) put("timestamp", Instant.now().toString()) } socket.emit("connection:reject", response) } ``` ## UI диалог на Android ### Пример диалога подтверждения ```kotlin private fun showConnectionRequestDialog( connectionId: String, operatorInfo: JSONObject, cameraType: String ) { val dialog = AlertDialog.Builder(this) .setTitle("Запрос доступа к камере") .setMessage(""" Оператор: ${operatorInfo.getString("name")} Организация: ${operatorInfo.getString("organization")} Причина: ${operatorInfo.getString("reason")} Камера: ${getCameraDisplayName(cameraType)} Разрешить доступ к камере? """.trimIndent()) .setPositiveButton("Разрешить") { _, _ -> acceptConnection(connectionId, cameraType) } .setNegativeButton("Отклонить") { _, _ -> rejectConnection(connectionId, "user_denied") } .setCancelable(false) .create() dialog.show() // Автоматическое закрытие через 5 минут Handler(Looper.getMainLooper()).postDelayed({ if (dialog.isShowing) { dialog.dismiss() rejectConnection(connectionId, "timeout") } }, 5 * 60 * 1000) } ``` ## Безопасность и валидация ### Проверки на сервере 1. **Валидация оператора**: проверка разрешений и статуса подключения 2. **Валидация устройства**: проверка доступности и возможности принять сессию 3. **Лимиты времени**: автоматическое завершение запросов через 5 минут 4. **Лимиты сессий**: проверка максимального количества активных сессий ### Проверки на Android 1. **Валидация connectionId**: проверка существования активного запроса 2. **Проверка разрешений**: доступ к камере и микрофону 3. **Проверка состояния**: доступность камеры для использования 4. **Защита от спама**: лимит на количество запросов в минуту ## Логирование и мониторинг ### События для логирования ```javascript // Сервер logger.info('Connection request initiated', { connectionId, operatorId, deviceId, cameraType }); logger.info('Connection accepted by device', { connectionId, sessionId, responseTime: Date.now() - connection.createdAt }); logger.warn('Connection rejected by device', { connectionId, reason, operatorId, deviceId }); logger.error('Connection timeout', { connectionId, operatorId, deviceId, duration: 5 * 60 * 1000 }); ``` ### Метрики для мониторинга - Время ответа Android устройств на запросы - Процент принятых/отклоненных подключений - Количество таймаутов - Средняя продолжительность сессий - Ошибки WebRTC соединений ## Совместимость Система поддерживает как новый протокол подключений (`connection:*` события), так и старый протокол (`camera:*` события) для обратной совместимости. ### Миграция со старого протокола 1. **Этап 1**: Добавление поддержки новых событий в Android 2. **Этап 2**: Постепенный переход операторов на новый API 3. **Этап 3**: Удаление старых обработчиков после полной миграции ## Примеры использования ### Тестирование через WebSocket ```javascript // Подключение к серверу const socket = io('ws://localhost:3001'); // Симуляция Android устройства socket.emit('register:android', { deviceId: 'test-device-001', deviceInfo: { manufacturer: 'Samsung', model: 'Galaxy S21', availableCameras: ['back', 'front'], androidVersion: '11' } }); // Обработка запросов socket.on('connection:request', (data) => { console.log('Получен запрос подключения:', data); // Автоматическое принятие для тестирования setTimeout(() => { socket.emit('connection:accept', { connectionId: data.connectionId, sessionId: data.sessionId, cameraType: data.cameraType, webrtcInfo: { supported: true, codecs: ['H264'], resolutions: ['1080p'] } }); }, 2000); }); ``` ### Тестирование через REST API ```bash # Инициация подключения curl -X POST http://localhost:3001/api/operators/connections/request \ -H "Content-Type: application/json" \ -H "x-operator-id: operator-uuid" \ -d '{ "deviceId": "test-device-001", "cameraType": "back" }' # Проверка статуса подключений curl -H "x-operator-id: operator-uuid" \ http://localhost:3001/api/operators/connections ``` Этот протокол обеспечивает надежную и безопасную систему запросов доступа к камере Android устройств с полным контролем пользователя над разрешениями.