Files
god_eye/docs/SERVER_TO_ANDROID_PROTOCOL.md
2025-10-04 11:55:55 +09:00

16 KiB
Raw Blame History

Протокол запросов от сервера к Android устройству

Обзор

Данный документ описывает протокол WebSocket событий, которые сервер отправляет на Android устройство для запроса подтверждения подключения оператора и открытия сеанса камеры.

Схема работы

  1. Оператор инициирует подключение через REST API или WebSocket
  2. Сервер создает соединение в ConnectionManager
  3. Сервер отправляет запрос на Android через WebSocket
  4. Android отображает диалог пользователю
  5. Пользователь принимает/отклоняет запрос
  6. Android отправляет ответ серверу
  7. Сервер уведомляет оператора о результате

События от сервера к 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)
}

Безопасность и валидация

Проверки на сервере

  1. Валидация оператора: проверка разрешений и статуса подключения
  2. Валидация устройства: проверка доступности и возможности принять сессию
  3. Лимиты времени: автоматическое завершение запросов через 5 минут
  4. Лимиты сессий: проверка максимального количества активных сессий

Проверки на Android

  1. Валидация connectionId: проверка существования активного запроса
  2. Проверка разрешений: доступ к камере и микрофону
  3. Проверка состояния: доступность камеры для использования
  4. Защита от спама: лимит на количество запросов в минуту

Логирование и мониторинг

События для логирования

// Сервер
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

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