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
natures/schemas.py— as naturesalbum,audiolivro,podcast,episodioecanal("obras de som e imagem").services/obras.py— metadados dos arquivos → saber (e de volta), os episódios de uma série, o feed RSS.enriquecimento/metadados_rede.py— MusicBrainz e Cover Art Archive.services/canal.py— a grade vira uma FUNÇÃO do relógio (agora,programacao,diagnostico_fontes).services/transmissor.py— a programação vira stream (HLS, Icecast).services/ao_vivo.py— RTMP, SRT e navegador; aSessao; a gravação.api/v3/canais.py— as rotas; nobbx-make-deb,debian-tree/usr/share/baobaxia/nginx/baobaxia-rtmp.conf.- No front:
GradeEditor,SeletorFontes,BuscaSaberes,CanalAoVivo,CanaisNoAr,AoVivoConexao,AoVivoNavegador, e oTocador. - 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:
| Nature | Partes | Onde ficam |
|---|---|---|
album, audiolivro | faixas / capítulos | os anexos, na ordem de data.ordem_conteudo; os dados de cada um em data.faixas[arquivo] |
podcast | episódios | cada 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_fontedesce 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 porCACHE_SEGe é single-flight — com muita gente abrindo a página, um calcula e os outros esperam o mesmo resultado.aleatorioembaralha 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(rotaGET /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. OSeletorFontesmostra 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 tiverBBX_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_chaveacha 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:
AoVivoNavegadorgrava comMediaRecorder(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
alimentarsó põe na fila; uma thread escreve; umafolga(BBX_AO_VIVO_FOLGA_MB) limita a memória; ebbx apisobe comws_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 oencerrando,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:
| Valor | Grade | Ao vivo | nginx :1935 |
|---|---|---|---|
on (padrão) | o gerente liga | aceita | aberto (com libnginx-mod-rtmp) |
manual | só bbx canal transmitir | aceita | aberto |
off | não | recusa (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
GradeEditormonta blocos, rotação, fuso, identidade;SeletorFontesescolhe as fontes com oBuscaSaberes(o buscador comum, com capa, tipo, data e galáxia) e mostra o diagnóstico de cada uma.- Na página do canal,
CanalAoVivotoca 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
Tocadorpublica 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 (
getantes de pôr na grade). - Mexeu na grade? Rode
test_canal_radio_tv.pyetest_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) etest_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 noREADME.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)?
