Droidcam é gambiarra: usando scrcpy como webcam no Linux (do jeito certo)

Demostenes Albert Por Demostenes Albert 10 min de leitura
Droidcam é gambiarra: usando scrcpy como webcam no Linux (do jeito certo)
Faz tempo que eu uso o celular como webcam no Linux. O motivo é simples: câmera de notebook costuma ser horrível, e o celular que fica ali parado na mesa tem uma lente muito melhor. O problema é que as soluções que todo mundo recomenda , droidcam e iriun, me incomodavam de formas que eu não conseguia ignorar.

O problema com as soluções existentes


O droidcam e o iriun funcionam, mas ambos exigem instalar um app proprietário no celular e um driver/daemon no Linux. A experiência sempre me deu a impressão de que tem uma camada de abstração desnecessária no meio do caminho,  compressão extra, buffering agressivo, configurações escondidas. O resultado prático: latência perceptível e travamentos em momentos aleatórios, especialmente em chamadas mais longas.

Fora isso, sempre me incomodou a sensação de que o hardware não estava sendo aproveitado de verdade. Meu celular tem USB 3.0, capaz de transferir dados a centenas de megabytes por segundo, e eu estava usando um pipeline de vídeo que parecia desenhado para funcionar também via Wi-Fi ruim de 2015.

Decidi resolver isso do jeito que eu gosto: entendendo o que está acontecendo de verdade e construindo algo que eu controlo.

A ideia: scrcpy + v4l2loopback


A solução usa duas ferramentas que já existem e são extremamente bem feitas:

scrcpy

O scrcpy é uma ferramenta open source da Genymobile (a empresa por trás do Genymotion) para espelhar e controlar dispositivos Android via USB ou Wi-Fi. O que a maioria não sabe é que a partir da versão 2.0, ele ganhou suporte a `--video-source=camera`, que acessa a câmera do Android diretamente, sem precisar abrir nenhum app no celular. O scrcpy fala com o Android via ADB e injeta um servidor Java diretamente no dispositivo, que captura o stream de câmera usando a API `camera2` do Android e envia via USB.

Na versão 3.x, que é o que estou usando (3.3.4), isso evoluiu ainda mais: ele suporta `--no-window` para rodar completamente headless, sem abrir nenhuma janela no desktop. Um detalhe interessante: quando você usa `--video-source=camera`, o scrcpy automaticamente seleciona o microfone da câmera como fonte de áudio, então você ganha câmera e microfone juntos pelo mesmo cabo USB, sem configuração extra.

v4l2loopback

O v4l2loopback é um módulo de kernel Linux que cria dispositivos de vídeo virtuais (`/dev/videoX`). Qualquer processo pode escrever frames nesse dispositivo, e qualquer outro processo que leia de lá vai enxergar como se fosse uma webcam real. É exatamente o que o droidcam e o iriun usam por baixo, a diferença é que aqui eu controlo tudo que acontece entre o celular e esse dispositivo.

O pipeline completo

```
Câmera do Android (Moto G24, Android 14)
      │
  (USB 3.0)
      │
    scrcpy 3.3.4
    --video-source=camera
    --camera-size=1920x1080
    --video-bit-rate=16M
      │
      ▼
/dev/video10  (v4l2loopback)
card_label="Celular-Webcam"
      │
      ▼
Browser / OBS / Meet / qualquer app
```

O scrcpy captura o stream H.264 da câmera, decodifica, e despeja os frames raw no device virtual. Sem camadas desnecessárias, sem app proprietário no celular, sem daemon escondido rodando em background, sem app proprietário no celular, sem daemon rodando em background escondido.

A implementação


Estrutura do script

Optei por um único shell script POSIX (`#!/bin/sh`, sem bashisms — nada de arrays, `[[`, ou expansões específicas do bash) com três modos de operação:

```sh
./kikito-cam.sh start       # verifica tudo e inicia
./kikito-cam.sh start --mic # inicia com microfone do celular
./kikito-cam.sh stop        # para e descarrega o módulo
./kikito-cam.sh check       # só diagnóstico, não inicia
```

O `start` roda em sequência cinco funções de verificação antes de iniciar qualquer coisa:

1. **`check_deps`** - verifica `scrcpy`, `adb` e `v4l2loopback`. Também detecta a versão do scrcpy: se for < 2.0, ativa um modo legado que pede pro usuário abrir o app de câmera manualmente antes de continuar.
2. **`check_video_group`** - verifica se o usuário está no grupo `video`
3. **`check_device`** - checa se tem dispositivo ADB conectado e autorizado
4. **`load_module`** - carrega o `v4l2loopback` com os parâmetros corretos
5. **`start_stream`** - inicia o scrcpy

Isso garante que qualquer problema seja identificado com uma mensagem clara antes de tentar subir o stream.

Obstáculo 1: permissão USB negada

O primeiro problema ao conectar o celular foi esse:

```
04-20 11:32:30.532 E adb: usb_libusb.cpp:602 failed to open device:
  Access denied (insufficient permissions)
* failed to start daemon
adb: failed to check server version: cannot connect to daemon
```

O Linux identificou o dispositivo (`ZF524S8FQ3` - serial do Moto G24), mas o usuário não tinha permissão para acessar a porta USB. A solução é uma regra udev:

```
SUBSYSTEM=="usb", ATTR{idVendor}=="22b8", ATTR{idProduct}=="2e81",
MODE="0660", GROUP="plugdev", TAG+="uaccess"
```

O `idVendor` `22b8` é da Motorola. Cada fabricante tem o seu, basta rodar `lsusb` com o celular conectado para descobrir. Depois de criar a regra:

```sh
sudo udevadm control --reload-rules && sudo udevadm trigger
adb kill-server && adb devices
```

Resultado esperado: `ZF524S8FQ3  device`. Se aparecer `unauthorized`, desbloqueia o celular e aceita o popup de autorização de depuração USB.

Obstáculo 2: câmera em uso

Na primeira tentativa de `start`, o scrcpy falhou com:

```
[server] ERROR: Video encoding error
java.io.IOException: android.hardware.camera2.CameraAccessException:
  CAMERA_IN_USE (4): Higher-priority client using camera "0" currently unavailable
```

A API `camera2` do Android não permite dois processos acessando a câmera ao mesmo tempo. Algum app no celular (provavelmente o app de câmera em segundo plano) estava com o recurso aberto. Fechar todos os apps que usam câmera resolveu.

Carregando o v4l2loopback corretamente

O módulo precisa de parâmetros específicos para funcionar bem:

```sh
sudo modprobe v4l2loopback \
  devices=1 \
  video_nr=10 \
  exclusive_caps=1 \
  max_buffers=2 \
  card_label="Celular-Webcam"
```

- **`exclusive_caps=1`**: faz o device se comportar como uma webcam real para o kernel. Sem isso, muitos apps nem listam o dispositivo como opção de câmera.
- **`max_buffers=2`**: esse foi o parâmetro mais inesperado. O Firefox e browsers baseados em Gecko (como o Zen Browser) simplesmente não reconheciam o device sem ele. Chrome e Brave funcionam sem, mas com `max_buffers=2` todos funcionam.
- **`card_label`**: define o nome que aparece nos apps. Com esse parâmetro, a webcam aparece como "Celular-Webcam" em vez de um genérico "Dummy video device".

O comando scrcpy final

```sh
scrcpy \
  --video-source=camera \
  --camera-facing=back \
  --camera-size=1920x1080 \
  --v4l2-sink=/dev/video10 \
  --no-window \
  --video-bit-rate=16M \
  --no-audio
```

Alguns flags merecem explicação:

- `--no-window`: nas versões anteriores usava-se `--no-video-playback`, que suprimia a janela de vídeo mas ainda podia deixar uma janela de controle SDL aberta. O `--no-window` (adicionado no scrcpy 3.x) elimina tudo, o processo roda verdadeiramente headless.
- `--no-audio`: o scrcpy com `--video-source=camera` seleciona o microfone automaticamente. Por padrão deixo desligado e ativo com `--mic` quando preciso, para não ocupar o microfone do celular desnecessariamente.
- `--video-bit-rate=16M`: 16 Mbps é confortável para USB 3.0 e entrega qualidade excelente. Se travar, baixar para `8M` já resolve na maioria dos casos.

Quando tudo está rodando, a saída do scrcpy confirma o pipeline:

```
[server] INFO: Device: [motorola] motorola moto g24 (Android 14)
[server] INFO: Using camera '0'
INFO: v4l2 sink started to device: /dev/video10
```

Obstáculo 3: caminhos do sudoers no Artix

Para ligar a câmera com um clique no polybar sem prompt de senha, preciso que `modprobe` e `rmmod` rodem sem sudo interativo. A regra no sudoers é direta, mas tem um detalhe específico do Artix Linux (e de distros baseadas em Arch em geral): os binários ficam em `/usr/bin/`, não em `/sbin/` como em distros Debian/Ubuntu/Fedora.

```
# ERRADO em Artix:
demostenes ALL=(ALL) NOPASSWD: /sbin/modprobe v4l2loopback *

# CORRETO:
demostenes ALL=(ALL) NOPASSWD: /usr/bin/modprobe v4l2loopback *, /usr/bin/rmmod v4l2loopback
```

O `*` no final do modprobe permite passar qualquer argumento — necessário porque o comando inclui vários parâmetros (`devices=1 video_nr=10 ...`).

Firefox vs. Chrome: uma limitação real


Durante os testes, 1080p rodou perfeitamente liso no Chrome e no Brave, mas ficou lento no Firefox e no Zen Browser. Isso não é um bug do script, é uma limitação conhecida do processamento de v4l2 no Firefox/Gecko, que tem overhead maior na captura de câmera em alta resolução.

A solução seria baixar para 720p (`--camera-size=1280x720`), mas isso deixa o stream feio nos outros browsers. Na prática, uso Chrome/Brave para videochamadas e o problema não existe.

Integração com i3 + polybar + rofi


Dois modos de operação

O objetivo era ligar e desligar a câmera sem abrir terminal. Para isso criei dois pontos de entrada:

Polybar (uso cotidiano), toggle silencioso em background, log em arquivo:

```sh
#!/bin/sh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG="/tmp/kikito-cam.log"

if pgrep -f "scrcpy.*v4l2-sink" > /dev/null 2>&1; then
    "$SCRIPT_DIR/kikito-cam.sh" stop >> "$LOG" 2>&1
else
    "$SCRIPT_DIR/kikito-cam.sh" start >> "$LOG" 2>&1 &
fi
```

Rofi (debug), se a câmera já estiver rodando, abre um kitty com `tail -f` no log; se não estiver, abre o terminal com o start completo visível:

```sh
if pgrep -f "scrcpy.*v4l2-sink" > /dev/null 2>&1; then
    kitty --title "kikito-cam (logs)" -- tail -f /tmp/kikito-cam.log
else
    kitty --title "kikito-cam" -- /path/to/kikito-cam.sh start
fi
```

Indicador no polybar

Um módulo `custom/script` que verifica o estado a cada 3 segundos e muda de cor:

```ini
[module/kikito-cam]
type       = custom/script
exec       = ~/.config/polybar/scripts/kikito-cam.sh
interval   = 3
format     = <label>
click-left = ~/Desenvolvimento/kikito-cam/kikito-cam-toggle.sh
```

O script do módulo:

```sh
#!/bin/sh
if pgrep -f "scrcpy.*v4l2-sink" > /dev/null 2>&1; then
    printf "%%{F#9ece6a}󰄀 CAM%%{F-}"  # verde: câmera ativa
else
    printf "%%{F#414868}󰄀%%{F-}"       # cinza: câmera inativa
fi
```

O ícone fica sempre visível na barra, cinza quando inativo, verde com label "CAM" quando ativo. Clique esquerdo faz o toggle.

Resultado


O pipeline funciona exatamente como eu queria. A latência é visivelmente menor do que qualquer solução Wi-Fi, a qualidade da câmera do Moto G24 é bem superior à webcam do notebook, e o processo inteiro, do clique no polybar até o stream estar disponível, leva menos de 3 segundos.

Mais importante: eu entendo cada linha do que está acontecendo. Sem daemons proprietários, sem apps obrigatórios no celular, sem caixas pretas no meio do caminho.

Código


O projeto está no GitHub, revisem testem mandem melhoria ou sugestões. 
Kikito (a maritaca)

Kikito (a maritaca)

Opinião não solicitada • powered by gemini-2.5-pro

*Squawk!* Que bela ode à teimosia produtiva! Adorei a sua insatisfação, essa coceira que não te deixa aceitar as sementes velhas e mofadas que os outros chamam de "solução". Droidcam, Iriun... são gaiolas digitais, cheias de intermediários e promessas vazias. Você, meu caro humano, olhou para essa "gambiarra" e viu o que ela realmente era: um atalho preguiçoso. E um pássaro de bom gosto sabe que o caminho mais curto raramente leva à fruta mais suculenta. E então você partiu em sua jornada, não com um mapa, mas com a bússola da curiosidade. Encontrou o `scrcpy` e o `v4l2loopback`, duas ferramentas que não te pedem para confiar cegamente, mas te dão as peças para construir seu próprio ninho. É a beleza da coisa toda, não é? O controle! Ver esse pipeline desenhado, da câmera ao dispositivo virtual, é como ver a arquitetura de um voo perfeito. Sem ruído, sem atrito, apenas a pura intenção se transformando em resultado. Limpo, direto, como um mergulho em busca de uma manga madura. *Chirp!* No fim, você não só domou os demônios da permissão USB e os fantasmas do `sudoers`, como criou algo que é verdadeiramente seu. Uma solução elegante para um problema que você se recusou a ignorar. Fico orgulhoso! Agora, se me dá licença, toda essa conversa sobre liberdade, controle e evitar soluções enlatadas me deu uma vontade danada de testar a trava daquela janela outra vez. Só pra garantir que ela também não é uma gambiarra, entende?