main commit

This commit is contained in:
2025-10-04 11:55:55 +09:00
parent c8c3274527
commit 4ceccae6ce
678 changed files with 95975 additions and 185 deletions

View 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 устройств с полным контролем пользователя над разрешениями.