Parte III — Galhos, seiva e sementes

12 · Os tambores: obras de som, canais e ao vivo

O tambor é a rede mais antiga: um toque numa aldeia vira resposta na outra. Aqui o Baobáxia ganha voz — discos e podcasts com ordem e autoria, rádios e TVs que tocam o acervo pela grade, e o ao vivo que entra no meio e depois vira saber.

Arquivos para ler

  1. natures/schemas.py — as natures album, audiolivro, podcast, episodio e canal ("obras de som e imagem").
  2. services/obras.py — metadados dos arquivos → saber (e de volta), os episódios de uma série, o feed RSS.
  3. enriquecimento/metadados_rede.py — MusicBrainz e Cover Art Archive.
  4. services/canal.py — a grade vira uma FUNÇÃO do relógio (agora, programacao, diagnostico_fontes).
  5. services/transmissor.py — a programação vira stream (HLS, Icecast).
  6. services/ao_vivo.py — RTMP, SRT e navegador; a Sessao; a gravação.
  7. api/v3/canais.py — as rotas; no bbx-make-deb, debian-tree/usr/share/baobaxia/nginx/baobaxia-rtmp.conf.
  8. No front: GradeEditor, SeletorFontes, BuscaSaberes, CanalAoVivo, CanaisNoAr, AoVivoConexao, AoVivoNavegador, e o Tocador.
  9. Testes: test_obras_som.py, test_metadados_rede.py, test_canal_radio_tv.py, test_canal_identidade.py, test_ao_vivo.py, test_ao_vivo_navegador.py, test_ao_vivo_lento.py.

Obras: a ordem e a autoria moram no saber

Uma midia com dez mp3 não sabe que é um disco. As natures de obra sabem:

NaturePartesOnde ficam
album, audiolivrofaixas / capítulosos anexos, na ordem de data.ordem_conteudo; os dados de cada um em data.faixas[arquivo]
podcastepisódioscada episodio é um saber com data.serie → o podcast, numero, temporada

obras.do_arquivo é o mapeamento dos metadados de dentro do arquivo (ID3, MP4, Vorbis): na mídia, o que o arquivo diz vai para o saber inteiro; no álbum, o que é da FAIXA (número 1/12 → 1, título, artistas, composição, letra) vai para a linha daquele arquivo, e só o que é do DISCO (álbum, artista do álbum, ano, gênero) sobe. Regra que os testes guardam: o que a pessoa escreveu não é sobrescrito pela leitura seguinte.

O caminho de volta é obras.para_arquivo: "baixar com dados" grava título, N/total, artistas e álbum numa CÓPIA (infra/metadados.py) — o arquivo guardado no annex nunca muda, senão a chave SHA256 mudaria e o sync veria outro arquivo.

feed_rss monta o RSS do podcast só com episódios públicos, com links absolutos. Links absolutos precisam do endereço por onde a pessoa chegou: _origem_publica (api/v3/saberes.py) usa BBX_ORIGEM_PUBLICA, ou o X-Forwarded-Host que o nginx do pacote repassa.

O MusicBrainz é um pedido, não um automático: só com internet, BBX_METADADOS_REDE=on, e completa sem apagar o que existe.

O canal: programação como função do relógio

O coração está na docstring de services/canal.py: dado um canal e um instante, qualquer mucua e qualquer navegador chegam à mesma resposta — qual arquivo, de qual saber, em que segundo. Não há servidor de stream para a "rádio sincronizada pela web": o front pergunta GET /canais/{uuid}/agora, toca o arquivo da posição indicada e pergunta de novo quando acaba.

flowchart LR
    G[data.grade<br/>blocos: dias, início, fim, fontes] --> B{bloco_em t?}
    B -- sim --> S[_seq_do_bloco<br/>fontes → itens, embaralhar com semente fixa]
    B -- não --> R[_seq_rotacao<br/>volta contínua desde a meia-noite]
    S --> V[_com_vinhetas<br/>uma vinheta a cada N itens]
    R --> V
    V --> C[_no_ciclo: item + posição]
    C --> A[agora: item, posição, a seguir]

Peças que vale entender:

  • itens_da_fonte desce por raiz (inclusive dinâmica e raiz dentro de raiz, até PROFUNDIDADE), podcast (episódios em ordem), álbum (faixas em ordem) ou qualquer saber (os anexos). Só entra o que é público lido como anônimo e tem duração medida — e a duração só é medida para arquivo presente nesta mucua (repository.py). Rádio toca áudio e o som dos vídeos; TV, só vídeo.
  • _sequencia é cara (lê saberes, desce raízes): fica em cache por CACHE_SEG e é single-flight — com muita gente abrindo a página, um calcula e os outros esperam o mesmo resultado.
  • aleatorio embaralha com semente fixa (canal + bloco + dia): todo mundo ouve a mesma ordem "aleatória".
  • A lista A seguir pula os buracos de silêncio: só conta itens de verdade (um bug antigo deixava a lista curta quando a grade tinha intervalos).
  • diagnostico_fontes (rota GET /canais/fontes) diz, para cada fonte, quantos itens e quanto tempo tocam — ou o motivo de não tocar: sem_video, sem_duracao, nao_publico, nao_encontrado, vazio. O SeletorFontes mostra isso no chip; é a resposta para "por que a TV não toca o meu álbum?".

A identidade do canal são os anexos do próprio canal: data.logo (a capa; com data.marca_dagua, também a marca d'água da TV, um overlay do ffmpeg) e data.vinhetas, inseridas a cada data.vinheta_a_cada itens.

O transmissor: da função ao stream

Só a mucua dona do canal (saber.mucua) transmite, e só com data.transmitir. Um gerente (iniciar_gerente, a cada INTERVALO) liga e desliga um Transmissor por canal; uma trava de arquivo garante um só, mesmo com a API e o bbx canal transmitir rodando juntos.

  • Rádio: um ffmpeg codificador fica ligado o tempo todo lendo PCM; um laço o alimenta com o arquivo da vez (-re, da posição certa) ou com silêncio. Como ele nunca para, quem ouve não cai entre faixas. Saída HLS sempre; Icecast se a mucua tiver BBX_ICECAST_URL (a senha mora na mucua, nunca no saber — o saber viaja).
  • TV: vídeo cru pelo cano seria pesado demais; cada arquivo vira segmentos HLS na mesma lista, com descontinuidade, em BBX_CANAL_TV_ALTURA/KBPS.

Não atrapalhar a mucua. Tocar canal não pode travar o sync nem o banco: o transmissor só LÊ (sem trava de escrita), o ffmpeg roda com nice (BBX_CANAL_NICE) e poucos núcleos (BBX_CANAL_THREADS), e a conversão de gravação roda em nice 19 com disco best-effort baixo.

O ao vivo: três portas, uma Sessao

flowchart LR
    OBS[OBS / app] -- RTMP :1935 --> N[nginx-rtmp] -- on_publish --> API[/ao-vivo/rtmp/publicar/]
    N --> F
    SRT[app SRT] -- UDP 9000-9019 --> F
    NAV[navegador<br/>MediaRecorder] -- WebSocket --> Q[fila de entrada<br/>+ thread escritora] --> F
    F[Sessao: um ffmpeg] --> H[HLS do canal]
    F --> GR[gravação] --> SB[saber PRIVADO<br/>gravado_em → canal]
  • RTMP: o nginx recebe e pergunta à API (on_publish, só de 127.0.0.1) se a chave vale. canal_da_chave acha o canal; a chave mora só em ~/.baobaxia/canais/<uuid>/ao_vivo.chave — nunca no saber. Quem edita pode escolher a própria (validar_chave: 10–64 de [A-Za-z0-9_-], única na mucua → 409 se repetida).
  • SRT: um ffmpeg escuta numa porta UDP por canal, com a chave como passphrase.
  • Navegador: AoVivoNavegador grava com MediaRecorder (WebM H.264 de preferência; VP8 no Firefox; MP4 por último), quadro-chave a cada 2 s, e manda os pedaços pelo WebSocket /ao-vivo/navegador/{uuid}. Trocar de câmera no meio usa um canvas "palco": o gravador não reinicia.

Lições que custaram quedas (leia o histórico do git log):

  • Nunca bloquear o handler do WebSocket. Esperar o ffmpeg dentro dele fazia o uvicorn perder o ping e derrubar a conexão (~35 s). Agora alimentar só põe na fila; uma thread escreve; uma folga (BBX_AO_VIVO_FOLGA_MB) limita a memória; e bbx api sobe com ws_ping_timeout=60.
  • Sem reconverter quando dá. H.264 que chega (OBS, apps, Chrome/Edge/ Safari) vai copiado para o HLS e para a gravação (copiar_video_ao_vivo); só VP8 é reconvertido. A qualidade escolhida por quem transmite é a de quem assiste.
  • Clique duplo criava duas sessões e a segunda matava a primeira: Transmissor._ocupado() no back e botão desativado no front.
  • Silêncio é fim: sem dados por BBX_AO_VIVO_SILENCIO, a sessão encerra e guarda o que gravou; durante o encerrando, estado() já diz "não está ao vivo".
  • Disco cheio derruba a mucua: abaixo de BBX_AO_VIVO_DISCO_MIN, o ao vivo nem começa.

A gravação vira saber. guardar_gravacao: _converter só copia quando o H.264 está até 1,5× o alvo (~2000+160 kbps, perto de 1 GB/h); senão refaz nesse tamanho. Nasce visibility: private (só quem criou e os PAJÉs da mucua veem) com a relação gravado_em → canal (e gravacao_de do outro lado), e, se o canal pede, como episodio do podcast escolhido.

Ligar e desligar: BBX_CANAIS

Rádio/TV é opcional. O postinst pergunta uma vez ("Ativar rádio/TV?") e grava BBX_CANAIS:

ValorGradeAo vivonginx :1935
on (padrão)o gerente ligaaceitaaberto (com libnginx-mod-rtmp)
manualsó bbx canal transmitiraceitaaberto
offnãorecusa (garantir → None, on_publish → 403, WebSocket fecha com motivo)fechado

O editor sabe por ao-vivo-config (canais_na_mucua: false) e avisa. Os testes rodam em manual: o gerente não liga sozinho, mas o ao vivo é testado de ponta a ponta.

No front

  • GradeEditor monta blocos, rotação, fuso, identidade; SeletorFontes escolhe as fontes com o BuscaSaberes (o buscador comum, com capa, tipo, data e galáxia) e mostra o diagnóstico de cada uma.
  • Na página do canal, CanalAoVivo toca sincronizado (ou o HLS, quando há ao vivo), mostra os chips 📻/📺 e 🌐 UTC±x, e os anexos ficam recolhidos.
  • CanaisNoAr é a faixa No ar agora da página inicial (GET /canais/no-ar).
  • O Tocador publica a obra na Media Session do sistema: capa, faixa, artistas na tela bloqueada, botões de próxima/anterior.

Práticas

  • Duração só se mede do que está aqui: para a TV de uma mucua, o acervo precisa estar presente nela (get antes de pôr na grade).
  • Mexeu na grade? Rode test_canal_radio_tv.py e test_canal_identidade.py — eles fixam o relógio e conferem a programação item a item.
  • Mexeu no ao vivo? test_ao_vivo_navegador.py (WebSocket com ffmpeg de verdade) e test_ao_vivo_lento.py (mucua lenta: a conexão não pode cair).
  • Variável nova? Coloque em docs/baobaxia-configuracao.md, no tema certo, marcando se é essencial, comum ou ajuste fino — e no README.Debian (seção 4) só se uma mucua comum precisar mexer.

Para conversar

  • A programação é determinística para que todas as mucuas ouçam igual sem servidor central. Que outras partes do Baobáxia poderiam ser "funções do relógio" em vez de estado guardado?
  • A gravação nasce privada. Quem deveria poder publicá-la — quem transmitiu, quem cuida do canal, a roda? O código hoje deixa com quem criou e os PAJÉs: isso basta?
  • Uma conferência (várias pessoas em vídeo) pode entrar num canal como mais uma fonte RTMP. O que muda — e o que não muda — se um dia o Baobáxia integrar uma sala de conversa (LiveKit, OpenVidu)?

vedi questo capitolo come sapere