JavaScript: autoplay bloqueado pelo navegador
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:
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
catche 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
Node.js: populando uma planilha no Google Sheets
Como escrever dados numa planilha do Google Sheets com Node.js, e as tres armadilhas que transformam dado certo em planilha errada.
Node.js: consultando CPF e CNPJ no SPC
Como consultar CPF e CNPJ no SPC Brasil com Node.js puro: envelope SOAP na mao, sem biblioteca, e as armadilhas do caminho.
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.