Introdução

Eu mantenho um app web de legendas ao vivo, o LiveToka, e a pergunta que mais recebia era sempre a mesma: 'como eu traduzo o vídeo do YouTube ou a reunião do Google Meet que está na minha outra aba?' A resposta clássica é um cabo de áudio virtual (VB-CABLE no Windows, BlackHole no Mac). Funciona, mas pedir para alguém instalar um driver de áudio faz a maioria desistir no meio do caminho. Navegadores baseados em Chromium resolvem isso sem extensão: navigator.mediaDevices.getDisplayMedia() deixa o usuário escolher uma aba e compartilhar o áudio dela. O exemplo básico cabe em três linhas. O que dá trabalho é o resto, e foi isso que aprendi ligando essa API a um pipeline de áudio em tempo real.

Capturar o áudio de outra aba com getDisplayMedia: o que a documentação não conta

Captura de áudio de outra aba

O usuário vê o seletor do navegador, escolhe uma aba, marca a opção de compartilhar o áudio da aba e você recebe um MediaStream. A partir daí, ele entra no Web Audio como se fosse um microfone:

  1. É preciso pedir vídeo, mesmo querendo só áudio
  • No Chrome, getDisplayMedia({ audio: true }) sem vídeo é rejeitado. Peça video: true e ignore a faixa de vídeo. Eu prefiro desativar a faixa em vez de encerrá-la.
  • Nos meus testes, encerrar a faixa de vídeo podia derrubar o compartilhamento inteiro em algumas configurações. Desativar é a opção conservadora e custa muito pouco.
  1. Permissão concedida não significa que veio áudio
  • Se o usuário escolher uma janela, ou esquecer de marcar o compartilhamento de áudio, a promise resolve normalmente com um stream sem nenhuma faixa de áudio. Verifique isso e explique o que aconteceu:
  • Depois mostre uma mensagem útil, algo como 'escolha uma aba e ative o compartilhamento de áudio'. E trate esse caso separado de NotAllowedError , que aqui costuma significar apenas que a pessoa fechou o seletor. Não é problema de permissão, então não mande ninguém mexer nas configurações do site.
  1. Precisa de um gesto do usuário, e código assíncrono consome esse gesto
  • getDisplayMedia tem que ser chamado durante uma ativação do usuário, ou seja, um clique. O meu handler de início fazia alguns await antes (uma chamada de API para abrir a sessão, a abertura do WebSocket) e só depois pedia o áudio. Com getUserMedia isso funciona, mas com captura de tela é frágil.
  • A solução: se o usuário escolheu áudio de aba, faça esse pedido logo no início do handler de clique, antes de qualquer outro await , e guarde o stream para o resto da configuração.
  1. Não faça o seletor reaparecer a cada reinício
  • Uma sessão ao vivo reinicia mais do que parece: o usuário troca o idioma, a conexão cai, o reconhecimento de fala precisa de outra configuração. Se cada reinício chamar getDisplayMedia de novo, o seletor reaparece no meio da transmissão, e ninguém gosta disso.
  • Mantenha o stream em nível de módulo e reutilize enquanto a faixa de áudio estiver viva:
  • O código de encerramento precisa então saber a diferença entre 'parar a sessão' (libera tudo) e 'reiniciar a sessão' (mantém a aba compartilhada). Acabei com um caminho explícito stop({ restart: true }) e uma limpeza que não encerra as faixas do stream de tela.
  1. Trate o botão "parar de compartilhar" do próprio navegador
  • O Chrome mostra uma barra para interromper o compartilhamento. Quando o usuário clica nela, a faixa de áudio dispara ended . Se você ignorar, o app continua "ouvindo" silêncio. Pare a sessão e diga por quê:
  • O mesmo vale para cabos virtuais usados via getUserMedia . No meu caso, virou uma opção "Áudio do computador" que desliga essas três flags.
  1. Desligue o processamento de voz nessa fonte
  • Os padrões de getUserMedia são pensados para uma pessoa falando no microfone: cancelamento de eco, supressão de ruído, ganho automático. Em áudio de aba eles atrapalham, porque música e vozes da aba podem ser tratadas como eco ou ruído. Peça o áudio limpo:
  • O mesmo vale para cabos virtuais usados via getUserMedia . No meu caso, virou uma opção "Áudio do computador" que desliga essas três flags.
  1. Estéreo, taxa de amostragem e o nó de processamento
  • O áudio de aba normalmente vem em estéreo a 48 kHz. createMediaStreamSource com um nó de processamento mono faz o downmix para você. Se a sua API de fala espera PCM mono de 16 kHz, faça o downsample nesse nó e converta para Int16. Eu ainda uso ScriptProcessorNode ali pela simplicidade, mas AudioWorklet é a escolha moderna e tira esse trabalho da thread principal.

Navegadores móveis: não há captura de áudio de aba. Esconda a opção no celular em vez de deixar falhar.
macOS: o Chrome oferece áudio de aba de forma confiável, mas capturar o áudio da tela inteira ou de apps desktop é mais limitado. Para apps desktop no Mac, um dispositivo virtual como o BlackHole continua sendo o caminho.
Windows: compartilhar a tela inteira pode incluir o áudio do sistema, que cobre também apps desktop como o Zoom.
Firefox e Safari: o suporte a áudio em getDisplayMedia varia. Consulte a tabela de compatibilidade do MDN para as versões atuais e use detecção de recurso em vez de olhar o user agent.

  1. Permita trocar a fonte no meio da sessão
  • Quando "microfone" e "áudio de aba" viram duas fontes atrás da mesma função, dá para alternar com a sessão rodando: desconecte o MediaStreamAudioSourceNode antigo, crie um a partir do novo stream e ligue no mesmo nó de processamento. Não precisa reconectar ao servidor, e o usuário corrige um "escolhi a fonte errada" sem começar de novo.
  • Peça video: true , desative a faixa de vídeo e confirme que existe uma faixa de áudio.
  • Faça a chamada primeiro no handler de clique.
  • Reutilize o stream entre reinícios e libere só em uma parada explícita.
  • Desligue cancelamento de eco, supressão de ruído e ganho automático nessa fonte.
  • Use detecção de recurso e ofereça um cabo virtual como alternativa onde não houver áudio de aba.

Para ver funcionando, abra um vídeo em uma aba e use a opção "Áudio de aba ou tela" do LiveToka em outra (link na fonte abaixo). Se você já esbarrou em outros problemas com essa API, conta aqui nos comentários.

Gere o JSON puro agora:"
"tags": ["getDisplayMedia", "navigator.

AudioCapturarContaGetdisplaymediaOutra