Servir HLS (HTTP Live Streaming) a partir do nginx é a espinha dorsal da maioria das configurações de streaming auto-hospedadas — VOD, catch-up e retransmissão ao vivo dependem disso. Este guia configura o nginx para servir segmentos HLS de forma eficiente, com os cabeçalhos de cache e CORS que os players realmente precisam.

Assumimos que você já está gerando playlists .m3u8 e segmentos .ts (ou .m4s) — por exemplo, a partir do pipeline FFmpeg NVENC.

Pré-requisitos

  • Ubuntu 22.04 com nginx instalado (apt install -y nginx)
  • Segmentos HLS sendo gravados em um diretório (ex: /var/www/hls)
  • Um domínio apontado para o servidor

Passo 1 — Bloco de localização HLS básico

Edite a configuração do seu site (/etc/nginx/sites-available/streaming):

server {
    listen 80;
    server_name stream.yourdomain.com;

    location /hls/ {
        # Serve segments from disk
        root /var/www;

        # Correct MIME types for HLS
        types {
            application/vnd.apple.mpegurl m3u8;
            video/mp2t ts;
            video/iso.segment m4s;
        }

        # CORS — required for browser players (hls.js, video.js)
        add_header Access-Control-Allow-Origin * always;
        add_header Cache-Control no-cache always;
    }
}

O Cache-Control: no-cache na playlist é importante — os players devem buscar novamente o .m3u8 para ver novos segmentos. Refinaremos o cache a seguir.

Passo 2 — Cache dividido: playlists vs segmentos

Playlists mudam a cada segmento; segmentos nunca mudam depois de gravados. Faça o cache deles de forma diferente:

location ~ \.m3u8$ {
    root /var/www;
    add_header Access-Control-Allow-Origin * always;
    add_header Cache-Control "no-cache, no-store" always;
    types { application/vnd.apple.mpegurl m3u8; }
}

location ~ \.(ts|m4s)$ {
    root /var/www;
    add_header Access-Control-Allow-Origin * always;
    # Segments are immutable — cache aggressively
    add_header Cache-Control "public, max-age=86400" always;
    types {
        video/mp2t ts;
        video/iso.segment m4s;
    }
}

Isso permite que CDNs e navegadores armazenem em cache os arquivos .ts pesados, enquanto sempre verificam novamente a pequena playlist.

Passo 3 — Habilitar sendfile e ajustar para throughput

Streaming é limitado pela largura de banda. No bloco http {} de /etc/nginx/nginx.conf:

sendfile on;
tcp_nopush on;
tcp_nodelay on;
sendfile_max_chunk 1m;

# Bump worker connections for high concurrency
worker_processes auto;
events {
    worker_connections 8192;
    use epoll;
    multi_accept on;
}

# Larger output buffers for large segments
output_buffers 4 256k;

sendfile on permite que o kernel copie arquivos de segmento diretamente do disco para o socket sem passar pelo userspace — uma grande vantagem para o serviço de segmentos de alta concorrência.

Passo 4 — Adicionar HTTPS (players exigem cada vez mais)

Navegadores bloqueiam conteúdo misto, então uma página em HTTPS não pode puxar HLS sobre HTTP. Obtenha um certificado com Certbot:

apt install -y certbot python3-certbot-nginx
certbot --nginx -d stream.yourdomain.com

O Certbot reescreve seu bloco de servidor para escutar na porta 443 com o certificado. Verifique a renovação automática:

certbot renew --dry-run

Passo 5 — Habilitar HTTP/2 (e HTTP/3 se possível)

HTTP/2 multiplexa requisições de segmento sobre uma única conexão — significativo para HLS onde um player puxa muitos arquivos pequenos:

listen 443 ssl;
http2 on;

Para HTTP/3 (QUIC), você precisa do nginx compilado com o módulo http_v3 ou uma compilação como nginx-quic. HTTP/3 ajuda mais os players em redes móveis com perdas.

Passo 6 — Lidar com o "thundering herd" em transmissões ao vivo

Quando um segmento de uma transmissão ao vivo popular é lançado, milhares de players o solicitam quase simultaneamente. Sem cuidado, todos acessam o disco de uma vez. Habilite o cache de arquivos abertos:

open_file_cache max=10000 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

Isso armazena em cache descritores de arquivo e metadados, de modo que a segunda até a milésima requisição para um segmento quente é servida do cache, e não de um stat()+open() novo.

Passo 7 — Teste

Recarregue e verifique:

nginx -t && systemctl reload nginx
curl -I https://stream.yourdomain.com/hls/stream.m3u8

Você quer 200 OK, Content-Type: application/vnd.apple.mpegurl, Access-Control-Allow-Origin: * e Cache-Control: no-cache na playlist. Carregue-a em um navegador com hls.js ou no VLC para confirmar a reprodução.

Solução de problemas

Erros de CORS no console do navegador: o cabeçalho Access-Control-Allow-Origin não está chegando ao cliente. Verifique se ele está configurado nas localizações .m3u8 e .ts, e se nenhum proxy upstream o remove.

Player trava / faz rebuffering: geralmente a entrega de segmentos não consegue acompanhar. Verifique a largura de banda com iftop; se a porta estiver saturada, você precisa de um uplink maior. Verifique também se hls_time (duração do segmento) não é muito curto — 6s é um padrão seguro.

404 nos segmentos, mas a playlist carrega: o caminho root está errado, ou os segmentos estão sendo excluídos ( hls_list_size curto) antes que os players os solicitem. Aumente hls_list_size no seu comando FFmpeg.

Próximos passos

Para entrega geo-distribuída, coloque uma frota de borda CDN na frente desta origem. Para o lado da transcodificação, consulte o guia FFmpeg NVENC. E dimensione a largura de banda da sua origem com nosso guia de velocidade de porta.