326 lines
12 KiB
Markdown
326 lines
12 KiB
Markdown
# Отладка: Приложение отправляет видео на сервер
|
||
|
||
## Проблема
|
||
|
||
**Симптомы:**
|
||
- ✓ Приложение подключается к серверу по WebSocket
|
||
- ✓ На экране видна картинка с камеры (превью работает)
|
||
- ❌ На сервере не получается видео
|
||
- ❌ В logcat нет сообщений об отправке видеофреймов
|
||
|
||
**Из logcat (2025-12-03 20:27:19):**
|
||
```
|
||
WebSocket: Connected! Response code=101
|
||
WebSocket: Header: Upgrade=websocket
|
||
WebSocket: Message received: {"error": "Invalid room or password"}
|
||
StreamViewModel: Status: Подключено к серверу ✓
|
||
StreamViewModel: Status: Получено: {"error": "Invalid room or password"}
|
||
```
|
||
|
||
**Но видеофреймы вообще не отправлялись!**
|
||
|
||
---
|
||
|
||
## Анализ
|
||
|
||
### Кто виноват?
|
||
|
||
Проблема была **в архитектуре приложения**, а не в WebSocket подключении.
|
||
|
||
#### 1. **CameraManager** - не захватывал фреймы
|
||
```kotlin
|
||
// ❌ ДО: только превью, никакой обработки фреймов
|
||
val preview = Preview.Builder().build()
|
||
imageCapture = ImageCapture.Builder().build()
|
||
|
||
// ✅ ПОСЛЕ: добавлен ImageAnalysis для захвата фреймов
|
||
val imageAnalysis = ImageAnalysis.Builder()
|
||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
|
||
.build()
|
||
.apply {
|
||
setAnalyzer(analysisExecutor) { imageProxy ->
|
||
processFrame(imageProxy) // Обработка каждого фрейма
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2. **MainActivity** - не передавал фреймы в ViewModel
|
||
```kotlin
|
||
// ❌ ДО: просто запускает камеру
|
||
cameraManager.startCamera(lifecycleOwner, pv.surfaceProvider, onError)
|
||
|
||
// ✅ ПОСЛЕ: передаёт фреймы в ViewModel
|
||
cameraManager.startCamera(
|
||
lifecycleOwner,
|
||
pv.surfaceProvider,
|
||
onError = { err -> Log.e("CameraManager", "Camera error: $err") },
|
||
onFrame = { frameData ->
|
||
viewModel.sendVideoFrame(frameData) // ← КЛЮЧЕВАЯ СТРОКА!
|
||
}
|
||
)
|
||
```
|
||
|
||
#### 3. **WebSocketManager** - использовал сложную рефлексию для бинарных данных
|
||
```kotlin
|
||
// ❌ ДО: сложная и хрупкая рефлексия
|
||
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)
|
||
|
||
// ✅ ПОСЛЕ: простой и надёжный API
|
||
import okio.ByteString.Companion.toByteString
|
||
val byteString = data.toByteString()
|
||
webSocket?.send(byteString)
|
||
```
|
||
|
||
---
|
||
|
||
## Решение
|
||
|
||
### Файл 1: CameraManager.kt
|
||
|
||
**Что изменилось:**
|
||
1. Добавлены импорты для `ImageAnalysis` и `ImageProxy`
|
||
2. Добавлены новые поля:
|
||
- `analysisExecutor` - выполняет анализ фреймов в отдельном потоке
|
||
- `onFrameAvailable` - callback для отправки фреймов
|
||
3. Изменена сигнатура `startCamera()`:
|
||
```kotlin
|
||
fun startCamera(
|
||
lifecycleOwner: LifecycleOwner,
|
||
previewSurfaceProvider: Preview.SurfaceProvider,
|
||
onError: (String) -> Unit,
|
||
onFrame: ((ByteArray) -> Unit)? = null // ← НОВЫЙ ПАРАМЕТР
|
||
)
|
||
```
|
||
4. Добавлен `ImageAnalysis` при привязке к камере
|
||
5. Новый метод `processFrame()`:
|
||
- Извлекает пиксельные данные из каждого фрейма
|
||
- Отправляет через callback `onFrameAvailable`
|
||
- Логирует количество фреймов в секунду
|
||
|
||
### Файл 2: MainActivity.kt
|
||
|
||
**Что изменилось:**
|
||
```kotlin
|
||
// Добавлена передача callback функции
|
||
cameraManager.startCamera(
|
||
lifecycleOwner,
|
||
pv.surfaceProvider,
|
||
onError = { err -> Log.e("CameraManager", "Camera error: $err") },
|
||
onFrame = { frameData -> // ← НОВЫЙ CALLBACK
|
||
viewModel.sendVideoFrame(frameData)
|
||
}
|
||
)
|
||
```
|
||
|
||
**Как это работает:**
|
||
1. Камера захватывает фрейм
|
||
2. `CameraManager` преобразует его в ByteArray
|
||
3. Вызывает callback `onFrame` с данными фрейма
|
||
4. ViewModel получает фрейм и отправляет на сервер через WebSocket
|
||
|
||
### Файл 3: WebSocketManager.kt
|
||
|
||
**Что изменилось:**
|
||
|
||
**Импорты:**
|
||
```kotlin
|
||
// ❌ ДО
|
||
import okhttp3.ByteString
|
||
|
||
// ✅ ПОСЛЕ
|
||
import okio.ByteString.Companion.toByteString
|
||
```
|
||
|
||
**Метод sendBinary():**
|
||
```kotlin
|
||
// ✅ НОВАЯ РЕАЛИЗАЦИЯ
|
||
fun sendBinary(data: ByteArray) {
|
||
try {
|
||
val byteString = data.toByteString()
|
||
webSocket?.send(byteString)
|
||
Log.d("WebSocket", "Binary data sent: ${data.size} bytes")
|
||
} catch (e: Exception) {
|
||
Log.e("WebSocket", "Binary send error: ${e.message}")
|
||
onError(e.message ?: "Failed to send binary data")
|
||
}
|
||
}
|
||
```
|
||
|
||
### Файл 4: StreamViewModel.kt
|
||
|
||
**Улучшено логирование:**
|
||
```kotlin
|
||
fun sendVideoFrame(frameData: ByteArray) {
|
||
try {
|
||
wsManager?.sendBinary(frameData)
|
||
|
||
// ... обновление статистики ...
|
||
|
||
if (currentTime - lastFpsTime >= 1000) {
|
||
Log.d("StreamViewModel", "FPS: $frameCount, Total bytes sent: $totalBytesTransferred")
|
||
_fps.value = frameCount
|
||
frameCount = 0
|
||
lastFpsTime = currentTime
|
||
}
|
||
} catch (e: Exception) {
|
||
Log.e("StreamViewModel", "Failed to send frame: ${e.message}", e)
|
||
}
|
||
}
|
||
```
|
||
|
||
### Файл 5: AndroidManifest.xml
|
||
|
||
**Удалены ненужные разрешения:**
|
||
```xml
|
||
<!-- ❌ УДАЛЕНЫ -->
|
||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||
|
||
<!-- ✅ ОСТАЛИСЬ -->
|
||
<uses-permission android:name="android.permission.CAMERA" />
|
||
<uses-permission android:name="android.permission.INTERNET" />
|
||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||
```
|
||
|
||
---
|
||
|
||
## Проверка в logcat
|
||
|
||
**После исправлений ожидаются следующие логи:**
|
||
|
||
### При подключении:
|
||
```
|
||
WebSocket: Connecting to: ws://192.168.0.112:8000/ws/client/HhfoHArOGcT/1
|
||
WebSocket: Connected! Response code=101
|
||
CameraManager: Camera started successfully with video streaming
|
||
StreamViewModel: Connected to server
|
||
```
|
||
|
||
### При потоке видео:
|
||
```
|
||
CameraManager: Processing 25 frames/5s, sending to server
|
||
WebSocket: Binary data sent: 12345 bytes
|
||
StreamViewModel: FPS: 25, Total bytes sent: 308640
|
||
WebSocket: Binary data sent: 12340 bytes
|
||
StreamViewModel: FPS: 25, Total bytes sent: 617280
|
||
```
|
||
|
||
---
|
||
|
||
## Результат
|
||
|
||
✅ **Проект успешно скомпилирован:**
|
||
```
|
||
BUILD SUCCESSFUL in 6s
|
||
97 actionable tasks: 15 executed, 82 up-to-date
|
||
```
|
||
|
||
✅ **Теперь приложение:**
|
||
1. Подключается к серверу
|
||
2. Захватывает видеофреймы с камеры
|
||
3. Отправляет их на сервер через WebSocket
|
||
4. Логирует скорость передачи (FPS и байты)
|
||
|
||
---
|
||
|
||
## Технические детали
|
||
|
||
### ImageAnalysis в CameraX
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ Camera Hardware │
|
||
└────────────┬────────────────────────────┘
|
||
│
|
||
┌────────┴──────────┬─────────────┐
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌─────────┐ ┌──────────┐ ┌──────────┐
|
||
│ Preview │ │ImageCapture ImageAnalysis│
|
||
│(Display)│ │(Photos) │ │(Frames) │
|
||
└─────────┘ └──────────┘ └──────────┘
|
||
│ │ │
|
||
└──────────────────┬┴─────────────┘
|
||
│
|
||
┌──────▼──────┐
|
||
│ GPU/Display │
|
||
└──────────────┘
|
||
|
||
CameraManager привязывает ВСЕ три use case одновременно:
|
||
bindToLifecycle(lifecycleOwner, cameraSelector,
|
||
preview, imageCapture, imageAnalysis)
|
||
```
|
||
|
||
### Поток видео
|
||
|
||
```
|
||
Camera → ImageAnalysis → processFrame()
|
||
↓
|
||
ByteArray → onFrameAvailable callback
|
||
↓
|
||
MainActivity → viewModel.sendVideoFrame()
|
||
↓
|
||
StreamViewModel → wsManager.sendBinary()
|
||
↓
|
||
WebSocketManager → okhttp3 WebSocket
|
||
↓
|
||
Server
|
||
```
|
||
|
||
---
|
||
|
||
## Файлы изменены
|
||
|
||
| Файл | Изменения |
|
||
|------|-----------|
|
||
| `CameraManager.kt` | +ImageAnalysis, +processFrame(), обновлена startCamera() |
|
||
| `MainActivity.kt` | +onFrame callback при запуске камеры |
|
||
| `WebSocketManager.kt` | Исправлен sendBinary(), заменена рефлексия на okio API |
|
||
| `StreamViewModel.kt` | Улучшено логирование FPS и размера данных |
|
||
| `AndroidManifest.xml` | Удалены ненужные разрешения |
|
||
|
||
---
|
||
|
||
## Как тестировать
|
||
|
||
1. **Запустите приложение** на устройстве/эмуляторе
|
||
2. **Введите параметры подключения:**
|
||
- IP сервера: `192.168.0.112`
|
||
- Порт: `8000`
|
||
- Room ID: `HhfoHArOGcT`
|
||
- Password: `1`
|
||
3. **Нажмите "Подключиться"**
|
||
4. **Откройте logcat** и отфильтруйте по:
|
||
- `CameraManager` - статус камеры
|
||
- `WebSocket` - отправка видео
|
||
- `StreamViewModel` - статистика FPS
|
||
5. **На сервере** должна появиться видео-трансляция
|
||
|
||
---
|
||
|
||
## Потенциальные улучшения
|
||
|
||
1. **Кодирование видео** - сейчас отправляются raw RGBA фреймы (очень большой размер)
|
||
- Используйте H.264 или VP9 кодирование
|
||
- Это уменьшит пропускную способность в 10-100 раз
|
||
|
||
2. **Качество и масштабирование**
|
||
- Добавить регулировку разрешения камеры
|
||
- Масштабировать фреймы перед отправкой
|
||
|
||
3. **Обработка ошибок**
|
||
- Переподключение при разрыве соединения
|
||
- Буферизация фреймов при медленной сети
|
||
|
||
4. **Производительность**
|
||
- Использовать `STRATEGY_BLOCK_CAPTURE_SESSION` если нужна синхронизация
|
||
- Оптимизировать работу потоков
|
||
|