16 KiB
16 KiB
Протокол запросов от сервера к Android устройству
Обзор
Данный документ описывает протокол WebSocket событий, которые сервер отправляет на Android устройство для запроса подтверждения подключения оператора и открытия сеанса камеры.
Схема работы
- Оператор инициирует подключение через REST API или WebSocket
- Сервер создает соединение в ConnectionManager
- Сервер отправляет запрос на Android через WebSocket
- Android отображает диалог пользователю
- Пользователь принимает/отклоняет запрос
- Android отправляет ответ серверу
- Сервер уведомляет оператора о результате
События от сервера к Android
1. connection:request - Запрос на подключение
Отправляется Android устройству когда оператор запрашивает доступ к камере.
// Сервер → 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)
Для совместимости со старой системой. Используется при прямом запросе камеры.
// Сервер → 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 - Переключение камеры
Запрос на переключение камеры во время активного сеанса.
// Сервер → Android
socket.emit('camera:switch', {
sessionId: 'uuid-session-id',
cameraType: 'front', // Новый тип камеры
timestamp: '2025-10-04T12:00:00.000Z'
});
4. camera:disconnect - Завершение сеанса
Уведомление о завершении сеанса камеры.
// Сервер → 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 - Принятие подключения
// 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 - Отклонение подключения
// 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)
// 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'
});
Жизненный цикл подключения
Успешное подключение
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 сеанса
Отклонение подключения
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)
Таймаут подключения
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
Инициация подключения
// В файле /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;
}
Принятие подключения
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 приложении
Регистрация обработчиков событий
// В 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)
}
Ответ на запрос подключения
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
Пример диалога подтверждения
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)
}
Безопасность и валидация
Проверки на сервере
- Валидация оператора: проверка разрешений и статуса подключения
- Валидация устройства: проверка доступности и возможности принять сессию
- Лимиты времени: автоматическое завершение запросов через 5 минут
- Лимиты сессий: проверка максимального количества активных сессий
Проверки на Android
- Валидация connectionId: проверка существования активного запроса
- Проверка разрешений: доступ к камере и микрофону
- Проверка состояния: доступность камеры для использования
- Защита от спама: лимит на количество запросов в минуту
Логирование и мониторинг
События для логирования
// Сервер
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: Добавление поддержки новых событий в Android
- Этап 2: Постепенный переход операторов на новый API
- Этап 3: Удаление старых обработчиков после полной миграции
Примеры использования
Тестирование через WebSocket
// Подключение к серверу
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
# Инициация подключения
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 устройств с полным контролем пользователя над разрешениями.