Pitch: capturar áudio de reunião no navegador sem bot (o que o getDisplayMedia() de fato entrega)
Estou construindo o LiveSuggest, um assistente de reunião que mostra sugestões enquanto a conversa ainda está rolando. Sem bot no Zoom ou no Teams; o navegador processa só o áudio que a pessoa escolhe compartilhar.
Chamar o LLM foi a parte fácil. O que me segurou semanas foi descobrir de onde vem o áudio. Tem combinação de sistema operacional e superfície de compartilhamento em que a faixa não existe.
A armadilha da janela
O caso clássico é compartilhar a janela do app de videoconferência. A prévia parece certa e o MediaStream chega. Só não vem faixa de áudio, e o navegador não avisa.
No código pedimos áudio de sistema explicitamente, como a documentação do Chrome recomenda:
navigator.mediaDevices.getDisplayMedia({
video: true,
audio: { systemAudio: "include" },
});
Isso não cria capacidade que o Chromium já não oferece naquela plataforma.
A matriz
| Superfície | Áudio |
|---|---|
| Aba do Chromium | sim, em desktop (checkbox costuma vir marcada) |
| Tela inteira | só Windows e ChromeOS, com opt-in manual |
| Janela | nunca |
No Windows, compartilhar a tela inteira pode levar o áudio do sistema: Teams desktop, Zoom desktop, qualquer app que esteja tocando som. Linux e macOS não expõem essa opção no navegador. No Mac, a saída prática é driver virtual (BlackHole, Loopback). No Linux, o caminho nativo que dá para documentar é reunião numa aba do Chromium (Meet, Teams web, Zoom web) e compartilhar essa aba.
Firefox e Safari não entregam áudio de aba comparável. Sob Linux sobrou Chromium, pacote da distro ou Chrome. Não gosto de escrever isso, mas não tenho plano B no produto.
Dá para montar null-sink no PipeWire e rerrotear o som. Funciona, e em Linux costuma ser mais limpo que um BlackHole no Mac. O LiveSuggest não faz isso, e não vou colar tutorial de pavucontrol num pitch.
Depois que o áudio chega
Aí o pipeline fica mais banal. AudioWorklet, PCM16 mono 16 kHz, pedaços pequenos numa WebSocket, STT em streaming. As sugestões só disparam em transcrições finalizadas, e o sistema pode ficar em silêncio.
A outra coisa que peguei foi não chamar o LLM em todo transcript. Conversa não chega fatiada em assuntos prontos. Acumulamos contexto e só geramos quando surgiu informação nova suficiente desde a última sugestão. Se a sugestão sai cedo demais, falta contexto; se sai tarde, o assunto já mudou. Subir a frequência só aumenta o ruído.
Dá para medir qualidade sem guardar o conteúdo das conversas (latência, tempo até o primeiro token, feedback explícito positivo/negativo/cópia). O texto da reunião não precisa ir para analytics.
Onde estou
O LiveSuggest ainda está em construção. O que mais me custou até aqui foi entender o que getDisplayMedia() promete em cada SO, antes de prometer qualquer coisa ao usuário final.
Artigo técnico mais longo (em inglês) com o pipeline completo: Building Real-Time AI Suggestions Without a Meeting Bot.