Files
cam_control_android/ARCHITECTURE_DIAGRAM.md
2025-12-09 21:21:26 +09:00

19 KiB
Raw Permalink Blame History

🎬 Визуальное объяснение проблемы и решения

ПРОБЛЕМА: Видео не отправляется на сервер

┌─────────────────────────────────────────────────────────────────────┐
│                      АРХИТЕКТУРА ДО ИСПРАВЛЕНИЯ                     │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────┐
│   📷 КАМЕРА │
└──────┬──────┘
       │ (RGBA frames)
       ▼
┌─────────────────────┐
│ CameraManager       │
│ • Preview только    │  ← ПРОБЛЕМА: ImageAnalysis отсутствует!
│ • Нет обработки     │
│ • Нет callback      │
└────────────────────┘

❌ РАЗОРВАНО ❌

┌──────────────────────┐
│ MainActivity         │
│ • Не передает       │  ← ПРОБЛЕМА: Нет callback для передачи фреймов
│   фреймы            │
└──────────────────────┘

❌ РАЗОРВАНО ❌

┌──────────────────────┐
│ StreamViewModel      │
│ • sendVideoFrame()  │  ← НИКОГДА НЕ ВЫЗЫВАЕТСЯ!
│   существует        │
│ • Но никто не       │
│   вызывает          │
└──────────────────────┘

❌ РАЗОРВАНО ❌

┌──────────────────────┐
│ WebSocketManager     │
│ • sendBinary() есть │  ← НИЧЕГО НЕ ОТПРАВЛЯЕТСЯ
│ • Но данных нет     │
└──────────────────────┘

❌ РАЗОРВАНО ❌

┌──────────────────────────┐
│ 🖥️ СЕРВЕР               │
│ • Нет видео ❌          │
└──────────────────────────┘

═══════════════════════════════════════════════════════════════════════

ПОТОК:   📷 → ✗ → ? → ✗ → ? → ✗ → 🖥️
                  ↑           ↑
             РАЗОРВАНО   РАЗОРВАНО

РЕШЕНИЕ: Соединяем цепь обработки видео

┌─────────────────────────────────────────────────────────────────────┐
│                    АРХИТЕКТУРА ПОСЛЕ ИСПРАВЛЕНИЯ                    │
└─────────────────────────────────────────────────────────────────────┘

┌──────────────────────┐
│    📷 КАМЕРА         │
│ (30 fps, 1920x1080) │
└──────────┬───────────┘
           │ RGBA frames
           ▼
╔══════════════════════════════════════╗
║  ✅ CameraManager                     ║
║  ├─ Preview (для экрана)            ║  ← НОВОЕ: ImageAnalysis
║  ├─ ImageCapture (снимки)           ║
║  └─ ✨ ImageAnalysis (потоковое)    ║  ← НОВОЕ: processFrame()
║     ├─ Analyzer: processFrame()      ║  ← НОВОЕ: onFrameAvailable callback
║     ├─ Convert → ByteArray           ║
║     └─ Invoke onFrameAvailable()     ║
╚════════════╤═════════════════════════╝
             │ ByteArray (фрейм)
             │
┌────────────▼──────────────────────────┐
│  ✅ MainActivity                      │
│  ├─ startCamera(...)                 │
│  └─ onFrame = { frameData →          │  ← НОВОЕ: callback функция
│      viewModel.sendVideoFrame(...)   │
│     }                                │
└────────────┬──────────────────────────┘
             │ ByteArray
             │
┌────────────▼────────────────────────────────────┐
│  ✅ StreamViewModel                             │
│  ├─ sendVideoFrame(frameData)                   │
│  ├─ wsManager.sendBinary(frameData)             │
│  ├─ frameCount++                                │
│  ├─ totalBytesTransferred += size               │
│  └─ Log: "FPS: X, bytes sent: Y"  ← УЛУЧШЕНО  │
└────────────┬─────────────────────────────────────┘
             │ ByteArray
             │
┌────────────▼──────────────────────────────────┐
│  ✅ WebSocketManager                         │
│  ├─ sendBinary(data)                        │
│  ├─ ByteString.toByteString() ← ИСПРАВЛЕНО │
│  ├─ webSocket.send(byteString)              │
│  └─ Log: "Binary data sent: X bytes"        │
└────────────┬───────────────────────────────────┘
             │ WebSocket Binary Frame
             │ (через интернет)
             ▼
┌──────────────────────────┐
│ 🖥️ СЕРВЕР               │
│ ✅ Видео получено!      │
│ • Фреймы приходят       │
│ • Отображаются          │
└──────────────────────────┘

═══════════════════════════════════════════════════════════════════════

ПОТОК:   📷 ✅ → CameraManager → MainActivity → ViewModel → WebSocket → 🖥️
                      ✅              ✅            ✅            ✅

Детальная диаграмма CameraManager

БЫЛО

┌─────────────────────────────────────────┐
│ startCamera()                           │
├─────────────────────────────────────────┤
│ 1. Get ProcessCameraProvider            │
│ 2. Create Preview (for display)         │
│ 3. Create ImageCapture (for photos)     │
│ 4. Select back camera                   │
│ 5. bindToLifecycle(                     │
│    preview,                             │
│    imageCapture                         │
│    ← НЕ ХВАТАЕТ АНАЛИЗАТОРА!           │
│   )                                     │
│ 6. Log "Camera started"                 │
└─────────────────────────────────────────┘

РЕЗУЛЬТАТ: 📷 → [Preview] → 🖥️
              → (ничего не выходит)

СТАЛО

┌─────────────────────────────────────────────────────┐
│ startCamera()                                       │
├─────────────────────────────────────────────────────┤
│ 1. Get ProcessCameraProvider                        │
│ 2. Create Preview (for display)                     │
│ 3. Create ImageCapture (for photos)                 │
│ 4. ✨ Create ImageAnalysis (for streaming)         │
│    ├─ setBackpressureStrategy(KEEP_ONLY_LATEST)   │
│    ├─ setOutputImageFormat(RGBA_8888)             │
│    └─ setAnalyzer(processFrame)                    │
│ 5. Select back camera                              │
│ 6. bindToLifecycle(                                │
│    preview,                                        │
│    imageCapture,                                   │
│    imageAnalysis ✨                                │
│   )                                                │
│ 7. Log "Camera started with video streaming"       │
└──────────────────────────────────────────────────────┘

РЕЗУЛЬТАТ: 📷 → [Preview] → 🖥️
              → [ImageAnalysis] → processFrame() → ByteArray → onFrameAvailable()

Диаграмма processFrame()

ImageAnalysis получает фрейм каждые ~33мс (30 FPS)

┌──────────────────────────────────────────────────┐
│ processFrame(imageProxy: ImageProxy)             │
├──────────────────────────────────────────────────┤
│                                                  │
│ frameCount++                                     │
│ currentTime = System.currentTimeMillis()         │
│                                                  │
│ if (currentTime - lastLogTime > 5000) {         │
│   Log "Processing frameCount frames/5s"         │
│   frameCount = 0                                │
│   lastLogTime = currentTime                     │
│ }                                               │
│                                                  │
│ ┌─────────────────────────────────────┐         │
│ │ ОБРАБОТКА ФРЕЙМА:                    │         │
│ │ 1. buffer = imageProxy.planes[0]    │         │
│ │ 2. buffer.rewind()                  │         │
│ │ 3. frameData = ByteArray(size)      │         │
│ │ 4. buffer.get(frameData)            │         │
│ │    = Копируем пиксели в ByteArray   │         │
│ └─────────────────────────────────────┘         │
│                                                  │
│ onFrameAvailable?.invoke(frameData)             │
│ ↓ Отправляем фрейм через callback ↓            │
│   (попадает в MainActivity.onFrame)             │
│                                                  │
│ imageProxy.close()                              │
│ (Освобождаем ресурсы)                          │
│                                                  │
└──────────────────────────────────────────────────┘

ПОТОК ДАННЫХ:
ImageProxy → Buffer → ByteArray → Callback → ViewModel → Server

Диаграмма MainActivity callback

БЫЛО

DisposableEffect(previewViewRef, isCameraRunning) {
    if (pv != null && isCameraRunning) {
        cameraManager.startCamera(
            lifecycleOwner,
            pv.surfaceProvider,
            onError = { err → ... }
            ← НЕ ПЕРЕДАЕМ ФРЕЙМЫ!
        )
    }
    onDispose { cameraManager.stopCamera() }
}

РЕЗУЛЬТАТ: Камера работает, но фреймы теряются!

СТАЛО

DisposableEffect(previewViewRef, isCameraRunning) {
    if (pv != null && isCameraRunning) {
        cameraManager.startCamera(
            lifecycleOwner,
            pv.surfaceProvider,
            onError = { err → ... },
            onFrame = { frameData →           ← НОВОЕ!
                viewModel.sendVideoFrame(frameData)
            }
        )
    }
    onDispose { cameraManager.stopCamera() }
}

РЕЗУЛЬТАТ: Каждый фрейм → ViewModel → Server ✅

Диаграмма StreamViewModel

┌────────────────────────────────────────────────┐
│ sendVideoFrame(frameData: ByteArray)            │
├────────────────────────────────────────────────┤
│                                                │
│ wsManager.sendBinary(frameData)               │
│   ↓ Отправляем через WebSocket ↓             │
│                                                │
│ frameCount++                                  │
│ totalBytesTransferred += frameData.size       │
│ _bytesTransferred.value = totalBytesTransferred│
│                                                │
│ if (currentTime - lastFpsTime >= 1000) {     │
│   ┌────────────────────────────────┐        │
│   │ КАЖДУЮ СЕКУНДУ:                │        │
│   │ Log "FPS: $frameCount, Bytes: $total" │
│   │ _fps.value = frameCount        │        │
│   │ frameCount = 0                 │        │
│   │ lastFpsTime = currentTime      │        │
│   └────────────────────────────────┘        │
│ }                                            │
│                                                │
└────────────────────────────────────────────────┘

СТАТИСТИКА КАЖДУЮ СЕКУНДУ:
- FPS: количество фреймов в секунду
- Total bytes: объем переданных данных

Диаграмма WebSocketManager

БЫЛО (рефлексия)

fun sendBinary(data: ByteArray) {
    val byteStringClass = Class.forName("okhttp3.ByteString")
    val ofMethod = byteStringClass.getMethod("of", ByteArray::class.java)
    val byteString = ofMethod.invoke(null, data)
    val sendMethod = WebSocket::class.java.getMethod("send", byteStringClass)
    sendMethod.invoke(webSocket, byteString)
}

ПРОБЛЕМЫ:
- Медленно (рефлексия)
- Хрупко (завязано на имена методов)
- Сложно (много промежуточных объектов)
- Может сломаться при обновлении OkHttp

СТАЛО (Clean API)

import okio.ByteString.Companion.toByteString

fun sendBinary(data: ByteArray) {
    val byteString = data.toByteString()  ← ОДИН ВЫЗОВ
    webSocket?.send(byteString)            ← ПРЯМОЙ API
}

ПРЕИМУЩЕСТВА:
- Быстро (прямой вызов)
- Надежно (стандартный API)
- Просто (две строки)
- Стабильно (часть OkHttp)

Таблица сравнения

Аспект БЫЛО СТАЛО
Захват видео Только превью ImageAnalysis + Preview
Обработка фреймов Нет processFrame()
Передача фреймов Разорвано onFrame callback
Отправка на сервер Никогда Каждый фрейм
Отправка бинарных данных Рефлексия okio.ByteString
Логирование Минимальное Подробное (FPS, bytes)
Результат Видео не идет Видео идет

Временная шкала обработки фрейма

0ms  → Камера захватывает фрейм (30fps = каждые 33мс)
2ms  → ImageAnalysis получает фрейм
3ms  → processFrame() преобразует в ByteArray
4ms  → Вызывается callback onFrameAvailable()
4ms  → MainActivity получает frameData
5ms  → viewModel.sendVideoFrame() вызывается
6ms  → StreamViewModel отправляет через WebSocket
7ms  → WebSocket отправляет на сервер
50ms → Сервер получает фрейм

Итого: ~50мс задержка (нормально для потоковой передачи видео)


Поток данных в памяти

1. ImageProxy (в памяти GPU)
   │
   ├─ ~10.7 МБ (1920×1080×4 байта для RGBA)
   │
   ▼
2. ByteArray (в памяти RAM)
   │
   ├─ ~10.7 МБ
   │
   ▼
3. okio.ByteString
   │
   ├─ ~10.7 МБ (временно)
   │
   ▼
4. WebSocket буфер
   │
   ├─ Фрагментируется по 16KB блокам
   │
   ▼
5. Socket отправляет
   │
   ├─ ~10.7 МБ/сек при 30fps
   │ (100 Мбит/сек требуется!)
   │
   ▼
6. Сервер получает

ОПТИМИЗАЦИЯ: Использовать H.264 кодирование
- 10.7 МБ → 0.1-0.5 МБ (100x сжатие)
- 10 Мбит/сек (вместо 100)

Рисунок создан: 2025-12-03
Для: Объяснения архитектуры видеопотока в CamControl
Статус: Все компоненты исправлены и соединены