App Android para transmissão ao vivo via RTMP com controle total da câmera pelo hardware Camera2 API e painel web de controle remoto acessível pela rede local.
Powered by: Kotlin + Camera2 API + RootEncoder + WebGUI embutida
| Camada | Tecnologia |
|---|---|
| Linguagem | Kotlin |
| Build | Gradle (Groovy + KTS) |
| Camera API | Android Camera2 API |
| Streaming | RootEncoder v2.4.5 via JitPack |
| Preview | OpenGlView (RootEncoder) |
| Servidor Web | WebControlServer — NanoHTTPD embutido porta 8080 |
| Protocolo principal | RTMP |
| Captura still RAW | DngCreator + ImageReader (RAW_SENSOR) |
| Serialização JSON | Gson (snake_case) |
| Persistência | SharedPreferences |
| Background | ForegroundService + WakeLock |
| Autenticação WebGUI | Cookie de sessão via WebControlAuth |
| Heartbeat remoto | HeartbeatService + HeartbeatLoop |
| Comandos remotos | RemoteCommandsService + CamuiRemoteCommandExecutor |
CamStreamerApp (Application)
│
└─► SplashActivity → LoginActivity / RegisterActivity
│
└─► MainActivity
│
├─ bind ──► StreamingService (ForegroundService)
│ │
│ ├─ RtmpStreamer (wrapper RootEncoder RtmpCamera2)
│ │ └─ Camera2Controller (Camera2 API + reflexão)
│ │ ├─ CameraCapabilitiesReader → CameraCapabilities
│ │ ├─ RawCaptureManager (DNG pipeline)
│ │ ├─ YuvFrameProcessor (processamento YUV)
│ │ └─ DepthFusionProcessor (depth output)
│ │
│ └─ WebControlServer (NanoHTTPD :8080)
│ ├─ WebControlAuth (sessões + cookies)
│ ├─ WebControlApi (rotas HTTP)
│ └─ WebControlHtml (HTML/JS/CSS inline → assets)
│
├─ GridOverlayView (overlay regra dos terços)
├─ HeartbeatService (keepalive remoto)
└─ RemoteCommandsService → CamuiRemoteCommandExecutor
| Arquivo | Responsabilidade |
|---|---|
CamStreamerApp.kt |
Application class (inicialização global) |
SplashActivity.kt |
Splash + redirect para Login ou MainActivity |
LoginActivity.kt |
Autenticação de usuário |
RegisterActivity.kt |
Cadastro de usuário |
MainActivity.kt |
HUD, FABs, Bottom Sheet, GestureDetector, GridOverlay |
StreamingService.kt |
ForegroundService — ciclo de vida do stream |
RtmpStreamer.kt |
Wrapper do RootEncoder RtmpCamera2 |
Camera2Controller.kt |
Núcleo da câmera — ISO, SS, WB, Zoom, Focus, OIS, EIS, Tonemap, RAW |
CameraCapabilitiesReader.kt |
Lê CameraCharacteristics e monta CameraCapabilities |
CameraCapabilities.kt |
Data class com todas as capacidades de um sensor |
RawCaptureManager.kt |
Pipeline DNG: DngCreator em HandlerThread dedicada |
YuvFrameProcessor.kt |
Processamento de frames YUV_420_888 |
DepthFusionProcessor.kt |
Fusão de depth output do sensor |
WebControlServer.kt |
NanoHTTPD — roteamento de requisições HTTP |
WebControlApi.kt |
Handlers das rotas: status, control, capabilities, RAW |
WebControlAuth.kt |
Geração e validação de cookies de sessão |
WebControlHtml.kt |
Serve index.html e assets da WebGUI |
GridOverlayView.kt |
View de grade de regra dos terços |
HeartbeatLoop.kt |
Loop de heartbeat periódico |
HeartbeatService.kt |
ForegroundService para heartbeat remoto |
RemoteCommandsService.kt |
Recebe e enfileira comandos remotos |
CamuiRemoteCommandExecutor.kt |
Executa os comandos recebidos no service |
- Android 8.0+ (API 26)
- Permissões:
CAMERA,RECORD_AUDIO,INTERNET,FOREGROUND_SERVICE - Android 13+:
POST_NOTIFICATIONS - Para salvar DNG em Android ≤ 9:
WRITE_EXTERNAL_STORAGE(maxSdkVersion=28)
# Clone o repositório
git clone https://github.com/luanscps/CAMSTREAMER-BR.git
cd CAMSTREAMER-BR
# Checkout da branch de desenvolvimento ativa
git checkout v5-CAMUIVia Android Studio:
1. File → Open
2. Selecione a pasta CAMSTREAMER-BR
3. Aguarde o Gradle sync automático
4. Run → Run 'app' (Shift+F10)
Via Terminal:
./gradlew installDebug1. Ative "Depuração USB" nas opções do desenvolvedor
2. Conecte via cabo USB
3. Aceite a autorização no celular
4. ./gradlew installDebug- Abra o app — conceda todas as permissões solicitadas
- Faça login ou cadastro
- Toque no ⚙️ para abrir o painel de configurações
- Insira a URL RTMP:
rtmp://192.168.1.100:1935/live/stream - Toque em Aplicar e depois no botão shutter para iniciar
Este app publica vídeo para um destino RTMP. Isso significa que ele não é o servidor final de distribuição: ele atua como encoder/publicador e precisa de um servidor ou software que receba a transmissão.
Em outras palavras:
CAMSTREAMER-BR (Android) ──RTMP publish──► Servidor RTMP (ex: MediaMTX) ──► Clientes/players
Você precisa de um receptor RTMP acessível pela rede. Exemplos:
- MediaMTX — opção recomendada para uso local, testes e distribuição multiprotocolo.
- Nginx + RTMP module.
- Wowza, Ant Media ou outro servidor compatível com RTMP.
Sem um servidor RTMP, o app não tem para onde enviar o stream. Nesse caso, apertar “Start Stream” apenas tentará conectar à URL configurada e falhará se não houver um endpoint ouvindo.
Para desenvolvimento, QA e uso em rede local, a opção mais simples é usar MediaMTX. Ele recebe RTMP e pode redistribuir o mesmo stream como RTSP, HLS e WebRTC, facilitando testes com VLC, navegador, OBS e FFplay.
Linux/macOS
curl -L https://github.com/bluenviron/mediamtx/releases/latest/download/mediamtx_linux_amd64.tar.gz | tar xz
./mediamtxWindows
- Baixe o release do MediaMTX no GitHub.
- Extraia o
.zip. - Execute
mediamtx.exe.
No CAMSTREAMER-BR, configure a URL no formato:
rtmp://IP_DO_SERVIDOR:1935/live/stream
Exemplo:
rtmp://192.168.1.100:1935/live/stream
Onde:
192.168.1.100= IP do computador/servidor rodando o MediaMTX1935= porta padrão do RTMPlive= path/applicationstream= nome/chave do stream
Depois de aplicar a URL:
- Abra o preview da câmera.
- Toque no botão de iniciar stream.
- O app vai publicar para o MediaMTX.
Se a conexão estiver correta, o stream passa a ficar disponível para leitura nos protocolos abaixo.
Depois que o MediaMTX receber o stream live/stream, você pode consumi-lo de várias formas.
rtmp://IP_DO_SERVIDOR:1935/live/stream
Exemplos:
ffplay rtmp://192.168.1.100:1935/live/stream
vlc rtmp://192.168.1.100:1935/live/streamrtsp://IP_DO_SERVIDOR:8554/live/stream
Exemplos:
ffplay rtsp://192.168.1.100:8554/live/stream
vlc rtsp://192.168.1.100:8554/live/streamhttp://IP_DO_SERVIDOR:8888/live/stream
Exemplo:
http://192.168.1.100:8888/live/stream
http://IP_DO_SERVIDOR:8889/live/stream
Exemplo:
http://192.168.1.100:8889/live/stream
Abra:
Mídia→Abrir fluxo de rede- Cole uma das URLs (
rtsp://...,rtmp://...ouhttp://...)
ffplay rtsp://192.168.1.100:8554/live/streamou
ffplay rtmp://192.168.1.100:1935/live/stream- Para HLS:
http://192.168.1.100:8888/live/stream - Para WebRTC:
http://192.168.1.100:8889/live/stream
No OBS, você pode adicionar uma Fonte de Mídia ou VLC Video Source apontando para:
rtsp://192.168.1.100:8554/live/stream- ou
rtmp://192.168.1.100:1935/live/stream
Isso é útil para usar o celular como câmera sem fio dentro de uma cena OBS.
Celular com CAMSTREAMER-BR ──► MediaMTX no PC ──► VLC / Navegador / OBS
Celular ──RTMP──► MediaMTX ──RTSP/RTMP──► OBS
Use o OBS para compor overlays, cenas, gravação local e retransmissão.
Celular ──RTMP──► MediaMTX ──HLS/WebRTC──► Navegadores na rede
Verifique:
- Se o MediaMTX está em execução.
- Se a porta 1935 está aberta.
- Se o IP configurado no app está correto.
- Se celular e servidor estão na mesma rede.
- Se a URL está no formato
rtmp://IP:1935/live/stream.
- Teste primeiro via RTSP ou RTMP no VLC/FFplay.
- Para browser, use 8888 para HLS e 8889 para WebRTC.
- Confirme que o path do stream é o mesmo (
live/stream).
Localização: Canto superior direito da tela
Exibe 8 métricas em tempo real (atualização a cada 1s):
| Métrica | Descrição |
|---|---|
| 🔆 ISO | Sensibilidade atual |
| ☀️ Exposição | Tempo de shutter atual |
| 🎯 Foco | Modo AUTO ou distância manual |
| 🎥 FPS | Taxa de quadros do encoder |
| 🌡️ Temperatura | Temperatura do dispositivo |
| 🔋 Bateria | Nível com cor semântica |
| 👥 Clientes | Clientes conectados ao painel web |
| 🌐 Rede | Tráfego de rede em MB/s |
Cores semânticas: 🟢 >50% / temp OK · 🟡 20–50% / temp elevada · 🔴 <20% / temp crítica
Localização: Lado esquerdo da tela (verticalmente)
| Botão | Ação |
|---|---|
| 📷 | Trocar câmera (ciclo: Wide → Ultra → Tele → Front) |
| 💡 | Flash/Lanterna on/off |
| 📸 | Captura de foto instantânea |
| 👁️ | Mostrar/ocultar HUD |
| ⚙️ | Abrir Bottom Sheet de controles avançados |
Ativação: Toque no FAB ⚙️ ou deslize de baixo para cima
- Seleção de câmera: 4 botões (Wide / Ultra / Tele / Front), ativo destacado em azul
- ISO: SeekBar 50–3200, valor em tempo real com fonte monoespaçada
- Exposição: SeekBar 1/8000s–1/15s
- Foco: SeekBar 0–100% — 0 = AUTO, >0 = distância MANUAL
- Locks: AE Lock 🔒 / AF Lock 🔒
- Flash: toggle ON/OFF
- Duplo toque no preview alterna entre UI completa e preview limpo (sem HUD/FABs)
- Long-press no preview ativa/desativa grade de regra dos terços (preferência salva em
SharedPreferences)
Com o app em foreground e serviço rodando, acesse:
http://<IP-DO-CELULAR>:8080
O IP local é exibido na barra de status do app.
⚠️ Autenticação: A WebGUI usa cookie de sessão viaWebControlAuth. Faça login com as credenciais configuradas no app antes de usar os controles.
Todas as rotas retornam JSON com Access-Control-Allow-Origin: *.
Retorna estado completo do sistema. Payload principal:
{
"streaming": true,
"rtmp_url": "rtmp://192.168.1.100:1935/live/stream",
"camera_id": "0",
"resolution": "1920x1080",
"bitrate_kbps": 4000,
"fps": 30,
"focus_mode": "continuous-video",
"focus_dist": 0.0,
"focus_distance_calibration": "CALIBRATED",
"iso": 800,
"exposure_ns": 16666666,
"frame_duration_ns": 33333333,
"manual_sensor": false,
"focal_length": 4.30,
"aperture": 1.5,
"zoom": 0.0,
"wb": "auto",
"ois": true,
"eis": false,
"ae_lock": false,
"awb_lock": false,
"torch": false,
"edge": "high_quality",
"nr": "high_quality",
"hot_pixel": "high_quality",
"rggb_enabled": false,
"rggb_r": 1.0, "rggb_gr": 1.0, "rggb_gb": 1.0, "rggb_b": 1.0,
"optical_zoom_index": 0,
"optical_zoom_levels": ["0.6x", "1x", "3x", "10x"],
"monitor": {
"iso": 800,
"shutter_ns": 16666666,
"af_state": 2,
"ae_state": 2,
"rggb_r": 2.1, "rggb_gr": 1.0, "rggb_gb": 1.0, "rggb_b": 1.8
},
"advanced_vision": {
"yuv_enabled": false,
"depth_enabled": false,
"last_yuv_ts_ns": 0,
"depth_mean_mm": 0.0,
"depth_min_mm": 0.0,
"depth_max_mm": 0.0,
"raw_ready": false,
"raw_filename": ""
},
"cameras": [ /* array de CameraCapabilities */ ]
}Envia um objeto JSON no body para alterar configurações em runtime.
| Parâmetro | Tipo | Exemplo | Observação |
|---|---|---|---|
streamAction |
String | "start", "stop", "restart" |
Controla o stream |
rtmpUrl |
String | "rtmp://..." |
Altera URL e reinicia stream |
camera / camera_id |
String | "0", "2" |
Troca câmera (dispatch no mainLooper) |
iso |
Int | 800 |
ISO manual (requer manualSensor=true) |
shutterSpeed / exposure_ns |
String/Long | "1/60" / 16666666 |
Velocidade do obturador |
frameDuration |
Long | 33333333 |
Duração do frame em ns |
exposure |
Int | 2 |
Compensação EV (modo auto) |
focus |
Float | 0.5 |
Distância de foco normalizada |
focusmode |
String | "af", "manual" |
Modo de foco |
afTrigger |
Boolean | true |
Dispara AF one-shot |
whiteBalance |
String | "auto", "daylight", "cloudy", "tungsten", "fluorescent", "shade" |
Modo AWB |
zoom |
Float | 0.0–1.0 |
Zoom mapeado para range real do sensor |
lantern |
Boolean | true/false |
Flash/lanterna |
flashMode |
Boolean | true/false |
Flash de captura |
ois |
Boolean | true/false |
Optical Image Stabilization |
eis |
Boolean | true/false |
Electronic Image Stabilization |
aeLock |
Boolean | true/false |
Trava de exposição automática |
awbLock |
Boolean | true/false |
Trava de balanço de branco |
bitrate |
Int | 4000000 |
Bitrate em bps — aplica stopStream+prepareVideo+startStream |
fps |
Int | 30 |
FPS do encoder |
resolution |
String | "4k", "1080p", "720p", "WxH" |
Resolução — aplica restart do stream |
manualSensor |
Boolean | true/false |
Habilita/desabilita sensor manual |
noiseReduction |
String | "OFF", "FAST", "HIGH_QUALITY", "MINIMAL" |
NR do ISP |
edgeMode |
String | "OFF", "FAST", "HIGH_QUALITY" |
Edge enhancement |
hotPixel |
String | "OFF", "FAST", "HIGH_QUALITY" |
Correção hot pixel |
tonemapCurve |
String | "linear", "s-curve", "log", "cinematic", "power22" |
Curva de tonemap (requer FULL) |
rggbEnabled |
Boolean | true/false |
Habilita ganhos RGGB manuais |
rggb_r / rggb_gr / rggb_gb / rggb_b |
Float | 1.8 |
Ganhos individuais do canal RGGB |
yuvCapture |
Boolean | true/false |
Habilita processador YUV |
depthFusion |
Boolean | true/false |
Habilita fusão depth |
Cast seguro: todos os parâmetros booleanos e inteiros aceitam tanto
true/falseJSON quanto0/1numérico — oCamera2Controllerconverte internamente para evitarClassCastException.
Retorna array com as capacidades de cada câmera detectada.
[
{
"camera_id": "0",
"hardware_level": "FULL",
"facing": "BACK",
"name": "Wide",
"supports_manual_sensor": true,
"supports_raw": true,
"iso_range": [50, 3200],
"exposure_time_range": [100000, 100000000],
"ev_range": [-8, 8],
"focus_distance_range": [0.0, 10.0],
"zoom_range": [1.0, 8.0],
"streaming_resolutions": ["3840x2160", "1920x1080", "1280x720"],
"raw_resolution": "4032x3024",
"supported_af_modes": ["off", "auto", "continuous-video"],
"supported_awb_modes": ["auto", "daylight", "cloudy", "tungsten"],
"has_flash": true,
"has_ois": true,
"focal_lengths": [4.30],
"apertures": [1.5]
}
]
streaming_resolutionscontém apenas as resoluções filtradas para streaming (máx 3, aspecto 16:9: 4K/1080p/720p).raw_resolutioné o tamanho real doImageReader RAW_SENSOR—nullse a câmera não suportar RAW.
Dispara captura RAW still. Retorna 202 Accepted imediatamente sem bloquear.
{ "status": "capturing", "message": "Captura iniciada. Consulte GET /api/raw/result?poll=1" }Importante: só funciona em câmeras com supports_raw: true no /api/capabilities.
Com ?poll=1 — polling leve (500ms recomendado), retorna JSON:
{ "ready": false, "filename": "", "size_kb": 0 }
// ou, quando pronto:
{ "ready": true, "filename": "raw_20260607_143022.dng", "size_kb": 22800 }Sem ?poll=1 — download binário do DNG (Content-Type: image/x-adobe-dng) se pronto, ou 404 se ainda não disponível. O cache é zerado após o download para evitar entregar a captura anterior em nova requisição.
O Camera2Controller gerencia uma sessão dedicada para captura RAW:
1. POST /api/raw/capture
└─► Camera2Controller.captureRawStill()
│
├─ Cria ImageReader(RAW_SENSOR, rawWidth, rawHeight, maxImages=4)
├─ Abre câmera com rawSurface inclusa na sessão (via cam2.addImageListener)
├─ Dispara session.capture() one-shot (TEMPLATE_STILL_CAPTURE)
│
├─ onImageAvailable → guarda Image ABERTA na pendingImageQueue
├─ onCaptureCompleted → drena fila, chama RawCaptureManager.processImage(image, result)
│
└─ RawCaptureManager.rawHandler.post {
DngCreator(characteristics, result).writeImage(outputStream, image)
→ image.close() ← único ponto de close, após DngCreator terminar
→ onRawFrame(RawFrame) ← notifica Camera2Controller
}
2. Camera2Controller.onRawFrame()
├─ lastRawDngBytes = frame.dngBytes
├─ lastRawFilename = "raw_YYYYMMDD_HHmmss.dng"
├─ saveToMediaStore() ← salva DNG em DCIM/CAMSTREAMER/
├─ captureLatch.countDown() ← libera thread de finalização
└─ startPreview() + startStream() ← retoma stream após sessão RAW
3. GET /api/raw/result?poll=1 ← polling 500ms pela WebGUI
4. GET /api/raw/result ← download do DNG binário
Requisitos de hardware:
supportsRaw = true(campo emCameraCapabilities). Não requersupportsManualSensor— câmerasLIMITEDque suportamRAW_SENSORfuncionam normalmente.
⚠️ Tonemap personalizado (CONTRAST_CURVE) requer hardware levelFULLou superior. Em dispositivosLIMITED/LEGACYo modo é ignorado silenciosamente.
data class CameraCapabilities(
val cameraId: String,
val hardwareLevel: String, // LEGACY, LIMITED, FULL, LEVEL_3
val facing: String, // BACK, FRONT, EXTERNAL
val name: String, // Wide, Ultra Wide, Telephoto, Frontal
val focusDistanceCalibration: String,// UNCALIBRATED, APPROXIMATE, CALIBRATED
// Flags de capacidades
val supportsManualSensor: Boolean,
val supportsManualPostProcessing: Boolean,
val supportsRaw: Boolean,
val supportsBurstCapture: Boolean,
val supportsDepthOutput: Boolean,
val supportsLogicalMultiCamera: Boolean,
// Ranges disponíveis
val isoRange: Pair<Int, Int>?,
val exposureTimeRange: Pair<Long, Long>?,
val evRange: Pair<Int, Int>?,
val focusDistanceRange: Pair<Float, Float>?,
val zoomRange: Pair<Float, Float>?,
val fpsRanges: List<Pair<Int, Int>>,
// Resoluções
val availableResolutions: List<String>, // todas as resoluções brutas YUV_420_888
val streamingResolutions: List<String>, // filtradas 16:9: máx 4K/1080p/720p
val rawResolution: String?, // resolução real do ImageReader RAW_SENSOR
// Modos suportados
val supportedAFModes: List<String>,
val supportedAEModes: List<String>,
val supportedAWBModes: List<String>,
// Hardware físico
val hasFlash: Boolean,
val hasOIS: Boolean,
val focalLengths: List<Float>,
val apertures: List<Float>
)| Câmera | Level | Zoom | Foco manual | ISO | RAW | Flash | OIS |
|---|---|---|---|---|---|---|---|
| Wide (ID 0) | FULL | ✅ 1×–8× | ✅ 0–10D | ✅ 50–3200 | ✅ | ✅ | ✅ |
| Frontal (ID 1) | LIMITED | ❌ fixa | ❌ fixo | ❌ | ❌ | ❌ | |
| Ultra Wide (ID 2) | LIMITED | ❌ fixo | ❌ | ❌ | ❌ | ||
| Telephoto (ID 3) | FULL | ✅ 2×–10× | ✅ | ✅ | ✅ | ❌ | ✅ |
| Preset | Uso | Comportamento |
|---|---|---|
linear |
RAW-like, sem processamento | y = x |
s-curve |
Blockbuster — contraste alto, blacks crushed | Interpolação cúbica (estilo DaVinci/ACES) |
log |
ARRI Alexa / RED — máximo dynamic range | y = c·log10(a·x + b) + d (Log-C) |
cinematic |
Blender Filmic — lift shadows, roll highlights | Piecewise com lift/cap |
power22 |
Standard sRGB para monitores | y = x^(1/2.2) |
Configurar via POST /api/control com {"tonemapCurve": "log"}.
# Linux/macOS
curl -L https://github.com/bluenviron/mediamtx/releases/latest/download/mediamtx_linux_amd64.tar.gz | tar xz
./mediamtx| Protocolo | URL |
|---|---|
| RTMP | rtmp://localhost:1935/live/stream |
| RTSP | rtsp://localhost:8554/live/stream |
| HLS | http://localhost:8888/live/stream |
Visualizar com VLC/FFplay:
ffplay rtmp://localhost:1935/live/streamOBS Studio: Configurações → Stream → Personalizado → rtmp://localhost:1935/live / chave stream
| Parâmetro | Chave API | Valores disponíveis |
|---|---|---|
| Edge Enhancement | edgeMode |
OFF, FAST, HIGH_QUALITY, ZERO_SHUTTER_LAG |
| Noise Reduction | noiseReduction |
OFF, MINIMAL, FAST, HIGH_QUALITY, ZERO_SHUTTER_LAG |
| Hot Pixel | hotPixel |
OFF, FAST, HIGH_QUALITY |
| Tonemap | tonemapCurve |
linear, s-curve, log, cinematic, power22 |
| Ganhos RGGB | rggbEnabled + rggb_r/gr/gb/b |
Float por canal |
### HUD
- [ ] HUD no canto superior direito, atualiza a cada 1s
- [ ] Cor da bateria muda (verde/amarelo/vermelho)
### FABs
- [ ] Trocar câmera com toast de confirmação
- [ ] Flash toggle funciona
- [ ] Foto capturada com feedback
- [ ] Toggle HUD mostra/oculta
### Bottom Sheet
- [ ] FAB ⚙️ abre/fecha bottom sheet
- [ ] Botão de câmera ativo destacado em azul
- [ ] Slider ISO atualiza em tempo real
- [ ] Slider Exposição atualiza em tempo real
- [ ] Slider Foco: 0 = AUTO, >0 = MANUAL
- [ ] Lock AE/AF alternam 🔒/🔓
### Modo Compacto
- [ ] Duplo toque alterna Normal ↔ Compacto
- [ ] Grade de regra dos terços via long-press, preferência persiste
### WebGUI
- [ ] Acesso em http://<IP>:8080 com login
- [ ] GET /api/status retorna JSON completo
- [ ] GET /api/capabilities retorna capacidades por câmera
- [ ] POST /api/control — start/stop/restart stream
- [ ] Controles aparecem/somem conforme câmera ativa
- [ ] Slider de bitrate sincroniza ao recarregar página
- [ ] Resoluções exibem apenas streaming_resolutions (máx 3)
### Captura RAW
- [ ] Card RAW aparece somente em câmera com supports_raw=true
- [ ] raw_resolution exibida corretamente
- [ ] POST /api/raw/capture retorna 202
- [ ] Polling ?poll=1 detecta quando raw_ready=true
- [ ] Download do DNG funciona no browser
- [ ] DNG salvo em DCIM/CAMSTREAMER/ no dispositivo
- [ ] Stream retoma normalmente após captura RAW
- [ ] Múltiplas capturas RAW em sequência sem crash./gradlew clean
./gradlew build- Verifique se a URL RTMP está correta (IP e porta 1935)
- Confirme que servidor (MediaMTX/OBS) está rodando
- Celular e servidor devem estar na mesma rede Wi-Fi
- Use
GET /api/capabilitiespara verificar o que a câmera suporta - Câmeras
LEGACYnão suportam controles manuais - Curvas de tonemap requerem hardware level
FULL
- Confirme o IP na barra de status do app
- O servidor HTTP inicia apenas com o service rodando em foreground
- Certifique-se de estar na mesma rede Wi-Fi
- Verifique
supports_rawem/api/capabilitiespara a câmera ativa - Acompanhe os logs:
adb logcat | grep -E "RawCaptureManager|Camera2Controller|WebControlApi" - Em dispositivos lentos, o poll pode precisar de mais de 10s para o DNG ficar pronto
- Certifique-se de que o app está em foreground durante a captura (a sessão RAW fecha o preview temporariamente)
Verifique em MainActivity.onCreate():
hudHandler.post(hudRunnable) // deve estar presenteadb logcat | grep "Camera2Controller"Ou no browser:
fetch('/api/capabilities').then(r => r.json()).then(console.log)Paleta de cores:
Primário: #38bdf8 (Azul Cyan)
Fundo: #0f172a (Azul Escuro)
Superfície: #1e293b (Cinza Azulado)
Texto: #f1f5f9 (Branco Suave)
Sucesso: #10b981 (Verde)
Alerta: #fbbf24 (Amarelo)
Erro: #ef4444 (Vermelho)
Tipografia:
- Headers: Sans-serif Bold 18sp
- Corpo: Sans-serif Regular 14sp
- HUD/Dados: Monospace 11–13sp
Captura RAW DNG
RawCaptureManager— pipeline dedicado comDngCreatoremHandlerThreadseparada- Sessão Camera2 dedicada para RAW via
cam2.addImageListener()(surface registrada antes do capture) - Pareamento
Image ↔ TotalCaptureResultviapendingImageQueue(evita race condition) Imagemantida aberta atéDngCreator.writeImage()terminar — sem alocação de 23 MB no heap- Salvamento em
MediaStore(Android 10+) ouFileOutputStream + MediaScannerConnection(≤ Android 9) - Retomada automática de
startPreview() + startStream()após sessão RAW
Rotas HTTP para RAW
POST /api/raw/capture— assíncrono, retorna 202 imediatamenteGET /api/raw/result?poll=1— polling leve em JSON sem baixar o arquivoGET /api/raw/result— download binário DNG com cache zerado após servir
WebGUI v5
- Card "Captura RAW Still" com botão, status e badge
raw_ready - Download via
fetch() + Blob URL(cross-origin compatível com Chrome) streaming_resolutionsfiltradas (16:9, máx 4K/1080p/720p) — backend não expõe resoluções brutasraw_resolutionexibida no card RAW viacap.raw_resolution- Slider de bitrate sincroniza com valor atual ao recarregar a página
- Remoção de toggle RAW órfão (apontava para função deletada —
ReferenceError)
Capabilities
- Novos campos:
streamingResolutions,rawResolution,focusDistanceCalibration supportsRawbaseado emRAW_SENSOR— independente desupportsManualSensor
Pipeline RAW (série de 15+ commits)
- ANR:
DngCreator.writeImage()movido paraHandlerThreaddedicada OutOfMemoryError: eliminadocopyAndClose()que alocava ~23 MB no heapmaxImages already acquired:Imagefechada somente nofinallydorawHandler.post{}- Deadlock:
rawReaderHandlererawImageHandleragora são threads separadas - Metadados errados no DNG:
TotalCaptureResultde preview não contamina mais a captura still IllegalArgumentException— surface RAW não estava naCameraCaptureSessionisRawCaptureFeasiblenão bloqueia mais porsupportsManualSensor- Download HTTP retornava sempre 404 —
lastRawDngBytesagora preenchido corretamente - Sessão RAW quebrava o stream —
startPreview()antes destartStream()no finally
Camera2Controller / WebGUI / Manifest
- Cast seguro para
Boolean/Int/Stringemois,eis,aeLock,awbLock,torch,rggbEnabled,manualSensor(evitaClassCastExceptioncom JSON numérico) bitrateeresolutionaplicados em runtime viastopStream + prepareVideo + startStream- Troca de câmera despachada no
mainLooper(evitaCalledFromWrongThreadException) - Handler de
exposure_ns/shutter_speedcomparseTimeParamcorreto AndroidManifest.xmlrestaurado após regressão que destruiu activities/serviços/permissões- Typo
CameraMetamer→CameraMetadata(causava falha de compilação)
- HUD de 8 métricas, FABs, Bottom Sheet (ISO/Exposição/Foco), modo compacto, grade de terços
- WebGUI completa portada para HTML/JS embutido no APK
- Controles Camera2 via reflection: ISO, SS, WB, Zoom, Focus, OIS, EIS, Bitrate, FPS
- WebControlServer HTTP na porta 8080
- Suporte RTMP via RootEncoder v2.4.5
- Sistema de capabilities dinâmicas (
discoverAllCameras)
- v2: Streaming RTSP + capabilities
- v1: Prova de conceito
- Conectar sliders de ISO/Exposição/Foco da UI nativa ao
Camera2Controller - Captura de foto JPEG funcional com salvamento na galeria
- Gravação de vídeo local simultânea ao stream
- Presets personalizados (Save/Load de configurações via SharedPreferences)
- Pinch-to-zoom no preview
- Gráfico de tráfego de rede em tempo real no HUD
- Cache de capabilities por câmera (evitar re-query a cada page load)
- Badge de hardware level na WebGUI (LEGACY/LIMITED/FULL/LEVEL_3)
- Slider de ISO dinâmico baseado no
iso_rangereal da câmera ativa - Conectar contadores de clientes e tráfego de rede do HUD ao
WebControlServer - Leitura real de FPS do encoder no HUD
- Leitura real de temperatura do sensor no HUD
- Testes automatizados de integração para o pipeline RAW em múltiplos devices
Desenvolvimento: Luan Silva (@luanscps)
Assistência IA: Perplexity AI
Stack: Kotlin + Material Design 3 + Camera2 API + RootEncoder