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

414 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🎬 Визуальное объяснение проблемы и решения
## ПРОБЛЕМА: Видео не отправляется на сервер
```
┌─────────────────────────────────────────────────────────────────────┐
│ АРХИТЕКТУРА ДО ИСПРАВЛЕНИЯ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────┐
│ 📷 КАМЕРА
└──────┬──────┘
│ (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
**Статус:**Все компоненты исправлены и соединены