main fixes

This commit is contained in:
2025-12-09 21:21:26 +09:00
parent 752b2fb1ca
commit 568ca73a11
33 changed files with 4353 additions and 345 deletions

413
ARCHITECTURE_DIAGRAM.md Normal file
View File

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