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