484 lines
16 KiB
Markdown
484 lines
16 KiB
Markdown
# Протокол запросов от сервера к 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 устройств с полным контролем пользователя над разрешениями. |