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