main commit
This commit is contained in:
484
docs/SERVER_TO_ANDROID_PROTOCOL.md
Normal file
484
docs/SERVER_TO_ANDROID_PROTOCOL.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# Протокол запросов от сервера к 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 устройств с полным контролем пользователя над разрешениями.
|
||||
Reference in New Issue
Block a user