Pular para o conteúdo
JavaScript

JavaScript: autoplay bloqueado pelo navegador

Paloma Macetko
Ilustração colorida de um unicórnio de crina luminosa diante de uma caixa de som trancada por um cadeado, com uma coruja de varinha e uma mão tocando um botão de play que solta ondas sonoras coloridas

Olá meus Unicórnios! 🦄✨

Sabe quando você coloca um autoplay no vídeo, abre a página e… nada acontece? 😅 Nenhum erro vermelho no console, nenhum aviso, nenhuma pista. O vídeo simplesmente fica lá, parado, fingindo que você nunca pediu nada.

Foi exatamente assim que eu perdi um tempo absurdo achando que era o caminho do arquivo. Ou o formato. Ou o servidor. Não era nada disso. Era o navegador dizendo "não" muito educadamente — tão educadamente que eu não estava escutando.

Este artigo é sobre esse "não": por que ele existe, como fazer o navegador contar o motivo, e o que dá para fazer para o som tocar mesmo assim. Com JavaScript puro, sem biblioteca nenhuma.

🔇 Por que o navegador bloqueia

A regra é simples de entender quando a gente lembra de como a web era: você abria uma página e bum, música. Ou pior — um anúncio em vídeo gritando, e você caçando qual das doze abas era a culpada. 🙉

Os navegadores cansaram disso e criaram uma regra que hoje vale em todos eles: som só toca depois que a pessoa interagir com a página. Um clique, um toque na tela, uma tecla. Enquanto isso não acontecer, o navegador segura.

Repare no detalhe que muda tudo: o bloqueio é sobre som, não sobre movimento. Vídeo mudo o navegador deixa passar numa boa — e é por isso que aquele vídeo de fundo do site bonitinho funciona sem você clicar em nada. Já voltamos nesse ponto. 😉

🕵️ O bug silencioso: o play() devolve uma Promise

Aqui mora a armadilha que me custou a tal tarde perdida. O play() não é um comando que dá certo ou estoura. Ele devolve uma Promise.

E o que acontece quando o navegador recusa? A Promise é rejeitada. Se ninguém estiver escutando essa rejeição, ela vira aquele erro fantasma que o console mostra pequenininho — quando mostra. Resultado: o áudio não toca e você não faz ideia do porquê.

Este é o código que causa o mistério:

// NAO faca assim: o bloqueio acontece e voce nunca fica sabendo
musica.play();

E este é o mesmo código, agora conversando com você:

const musica = document.getElementById('musica');

// O play() devolve uma Promise. Sem o await dentro de um try/catch,
// o bloqueio vira uma rejeicao que ninguem trata e o audio so nao toca.
async function tentarTocar() {
  try {
    await musica.play();
    console.log('tocou: o navegador deixou');
  } catch (erro) {
    console.log('bloqueou name: ' + erro.name);
    console.log('bloqueou message: ' + erro.message);
  }
}

tentarTocar();

É só isso. Um await dentro de um try/catch. E de repente o navegador para de ser misterioso e começa a explicar o que ele quer. 🙏

📢 A mensagem que o Chrome realmente dá

Com o try/catch no lugar, o navegador finalmente falou. E é aqui que vem a parte interessante: a mensagem não é a que a maioria dos tutoriais cita.

Montei uma página mínima — um <audio>, um botão, e a função lá de cima. Ela tenta tocar sozinha assim que carrega, e tenta de novo quando você clica. No Chrome 150, a tentativa sem clique nenhum devolveu isto:

NotAllowedError
play() failed because the user didn't interact with the document first. https://goo.gl/xX8pDD

Repare em duas coisas. A primeira: o name do erro é NotAllowedError — é por ele que você deve programar, porque é o nome padronizado. A segunda: a message do Chrome é bem direta ("o usuário não interagiu com o documento primeiro") e ainda vem com um link explicando a política.

E a mensagem "The request is not allowed by the user agent or the platform in the current context…", que aparece em tanto lugar? Ela é o texto genérico de NotAllowedError que outros contextos usam. O Chrome não usa esse texto para o bloqueio de autoplay. Por isso a regra de ouro: trate pelo name, nunca pelo texto da message — o texto muda entre navegadores e entre versões. O Firefox recusa com um texto diferente, mas com o mesmo name.

A página com as duas tentativas lado a lado, em vermelho o bloqueio e em verde o que aconteceu depois do clique:

Console mostrando em vermelho o erro NotAllowedError com a mensagem play() failed because the user didn't interact with the document first, e em verde a linha com gesto tocou: o navegador deixou

A mesma função, o mesmo arquivo de áudio, o mesmo navegador. A única diferença entre a linha vermelha e a verde é que alguém clicou. 🖱️

🎭 NotAllowedError não é NotSupportedError

Esses dois se confundem demais, e confundir custa caro porque a solução de um não tem nada a ver com a do outro:

  • NotAllowedError — é permissão. O arquivo está lá, o formato é válido, tudo certo: o navegador só não quer tocar agora. Some no primeiro gesto do usuário.
  • NotSupportedError — é formato (ou caminho). O navegador não consegue tocar aquele arquivo, e clicar mil vezes não vai mudar isso. Aqui você troca o codec, oferece um <source> alternativo, ou conserta a URL.

Se você tratar os dois iguais, vai ficar mostrando "clique para ouvir" para um usuário cujo navegador nunca vai tocar aquele arquivo. 😬 Por isso vale separar:

function explicarErro(erro) {
  if (erro.name === 'NotAllowedError') {
    return 'O navegador bloqueou o som. Clique para ouvir.';
  }
  if (erro.name === 'NotSupportedError') {
    return 'Este formato de audio nao e suportado aqui.';
  }
  return 'Nao consegui tocar: ' + erro.name;
}

🔀 Um terceiro erro que aparece sem avisar: AbortError

Esse eu não estava procurando — ele apareceu sozinho enquanto eu testava, e achei ótimo. 😄

Se você chamar play() e, antes de ele terminar, chamar pause() (ou trocar o src), o navegador aborta a reprodução que estava começando. O erro que chega é:

AbortError
The play() request was interrupted by a call to pause(). https://goo.gl/LdLk22

Ou seja: nem todo erro no catch é bloqueio. Esse aqui é você mesmo atrapalhando o navegador — clássico de acontecer quando dois pedaços de código mexem no mesmo player, ou quando um botão dispara play() e outro pause() em sequência rápida. Mais um motivo para olhar o name antes de sair mostrando "clique para ouvir".

🔕 O truque do muted, e por que ele é aceito

Lembra que o bloqueio é sobre som? Então a saída mais simples é justamente essa: vídeo mudo o navegador libera.

<video src="video.mp4" autoplay muted playsinline></video>

O raciocínio do navegador é honesto: se não sai som, ninguém é incomodado. Aquilo que motivou a regra toda — a música surpresa no meio do escritório — simplesmente não acontece. Então pode passar. 🤫

O playsinline entra junto porque, sem ele, o iPhone abre o vídeo em tela cheia em vez de tocar ali no meio da página. É o par que faz o vídeo de fundo funcionar de verdade no celular.

E se você quiser som depois? Aí vale o desenho clássico: começa mudo, e oferece um botãozinho de "ativar som". O muted = false dentro do clique é permitido, porque aí já houve gesto.

👆 Liberando no primeiro clique

O padrão mais útil de todos: em vez de tentar tocar e desistir, você espera o primeiro gesto — qualquer um — e toca ali.

function liberarNoPrimeiroClique() {
  // { once: true } faz o proprio navegador remover o ouvinte depois
  // da primeira vez. Sem isso, cada clique na pagina tentaria tocar.
  document.addEventListener('click', tentarTocar, { once: true });
}

async function tentarTocar() {
  try {
    await musica.play();
  } catch (erro) {
    console.log(explicarErro(erro));
  }
}

// tenta ja; se o navegador recusar, espera o primeiro clique
tentarTocar();
liberarNoPrimeiroClique();

Repare que ele tenta as duas coisas: tenta tocar de cara (porque às vezes o navegador deixa, já já explico por quê) e, em paralelo, deixa o gesto armado para quando o usuário encostar na página.

⏱️ O detalhe cruel: o gesto vale para o momento, não para sempre

Essa é a pegadinha que faz gente jurar que "não funciona". O gesto libera a chamada que acontece naquele momento. Se você adiar, o crédito já era:

botao.addEventListener('click', function () {
  // ARMADILHA: quando o setTimeout disparar, o gesto ja passou
  // e o navegador trata como se ninguem tivesse clicado.
  setTimeout(function () {
    musica.play();   // volta o NotAllowedError
  }, 2000);
});

O mesmo vale para um await fetch(...) antes do play(): enquanto a requisição vai e volta, o gesto expira. A saída é chamar play() primeiro e deixar o resto para depois:

botao.addEventListener('click', async function () {
  // toca agora, enquanto o gesto ainda vale
  await tentarTocar();

  // so depois vai buscar o que precisar
  const resposta = await fetch('/api/faixa');
  const dados = await resposta.json();
  console.log(dados.titulo);
});

🎚️ O AudioContext nasce suspenso

Se em vez de <audio> você usa a Web Audio API, a regra aparece de outro jeito — e no começo parece até que nada quebrou. O AudioContext é criado no estado suspended, e nesse estado ele não produz som nenhum, mesmo com todo o resto certo.

Na minha página de teste, criar o contexto sem nenhum gesto mostrou exatamente isso:

estado do AudioContext: suspended

Não vem erro, não vem aviso — vem silêncio. O conserto é chamar resume() dentro do gesto:

const contexto = new AudioContext();

botao.addEventListener('click', async function () {
  // resume() precisa acontecer dentro do gesto, igual ao play()
  if (contexto.state === 'suspended') {
    await contexto.resume();
  }
  console.log('estado agora: ' + contexto.state);
});

🎲 Por que às vezes toca sem clique nenhum

Esse foi o achado que mais me pegou de surpresa, e ele explica muita confusão. 😳

Enquanto eu testava, o bloqueio parou de acontecer. Mesma página, mesmo código, mesmo navegador — e o áudio começou a tocar sozinho, sem clique nenhum. Parecia que eu tinha quebrado o teste.

Não tinha. O Chrome guarda uma espécie de histórico de confiança por site: se você já tocou mídia naquele endereço algumas vezes, ele passa a liberar a reprodução automática ali. Como eu tinha acabado de tocar o áudio clicando no botão, o site "ganhou o crédito" — e as tentativas seguintes passaram direto.

Duas consequências bem práticas:

  • Testar autoplay no site que você usa todo dia mente para você. Ele provavelmente já tem crédito. Para ver o bloqueio de verdade, é preciso um endereço que o navegador ainda não conhece — foi assim que eu consegui reproduzir a mensagem lá de cima de novo.
  • Nunca conte com o autoplay funcionando. Ele pode funcionar na sua máquina e falhar na do usuário só porque ele nunca visitou seu site antes. O código com catch e o botão de reserva não são preciosismo: é o caminho normal. 🙂

🧾 A página inteira, para copiar

Junta tudo: tenta tocar, mostra o motivo quando não dá, e libera no clique.

<audio id="musica" src="som.wav"></audio>
<button id="botao">Tocar</button>

<script>
const musica = document.getElementById('musica');

function explicarErro(erro) {
  if (erro.name === 'NotAllowedError') {
    return 'O navegador bloqueou o som. Clique para ouvir.';
  }
  if (erro.name === 'NotSupportedError') {
    return 'Este formato de audio nao e suportado aqui.';
  }
  if (erro.name === 'AbortError') {
    return 'A reproducao foi interrompida por um pause().';
  }
  return 'Nao consegui tocar: ' + erro.name;
}

// Sem o await dentro do try/catch, o bloqueio some sem deixar rastro.
async function tentarTocar() {
  try {
    await musica.play();
    console.log('tocou');
  } catch (erro) {
    console.log(explicarErro(erro));
  }
}

// tenta de cara; se recusar, o primeiro clique resolve
tentarTocar();
document.addEventListener('click', tentarTocar, { once: true });

document.getElementById('botao').addEventListener('click', tentarTocar);
</script>

São umas trinta linhas, sem biblioteca nenhuma, só API do navegador. E elas resolvem o problema inteiro: quando dá para tocar, toca; quando não dá, você sabe o motivo em vez de ficar caçando fantasma. 💜

📌 O resumo em três linhas

  • Trate a Promise do play() — sem isso o bloqueio é invisível.
  • Decida pelo erro.name, nunca pelo texto da mensagem.
  • Vídeo mudo passa; som precisa de gesto, e o gesto vale só naquele instante.

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟

Leia também

JavaScript

Testando câmera e microfone no navegador

Como pedir permissão, mostrar a prévia, gravar e reproduzir áudio e vídeo direto do navegador, com JavaScript puro e sem nenhuma biblioteca.