Part III — Branches, sap and seeds
12 · The drums: works of sound, channels and going live
The drum is the oldest network: a beat in one village becomes an answer in the next. Here Baobáxia gains a voice — albums and podcasts with order and authorship, radios and TVs that play the archive from a schedule, and live broadcasts that cut in and then become saberes.
Files to read
natures/schemas.py— thealbum,audiolivro,podcast,episodioandcanalnatures ("works of sound and image").services/obras.py— file metadata → saber (and back), a series' episodes, the RSS feed.enriquecimento/metadados_rede.py— MusicBrainz and Cover Art Archive.services/canal.py— the schedule becomes a FUNCTION of the clock (agora,programacao,diagnostico_fontes).services/transmissor.py— the programme becomes a stream (HLS, Icecast).services/ao_vivo.py— RTMP, SRT and browser; theSessao; the recording.api/v3/canais.py— the routes; inbbx-make-deb,debian-tree/usr/share/baobaxia/nginx/baobaxia-rtmp.conf.- In the front:
GradeEditor,SeletorFontes,BuscaSaberes,CanalAoVivo,CanaisNoAr,AoVivoConexao,AoVivoNavegador, and theTocador. - Tests:
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.
Works: order and authorship live in the saber
A midia with ten mp3s doesn't know it is an album. The work natures do:
| Nature | Parts | Where they live |
|---|---|---|
album, audiolivro | tracks / chapters | the attachments, in data.ordem_conteudo order; each one's data in data.faixas[file] |
podcast | episodes | each episodio is a saber with data.serie → the podcast, numero, temporada |
obras.do_arquivo maps the metadata inside the file (ID3, MP4, Vorbis): in a media saber, what the file says goes to the whole saber; in an album, what belongs to the TRACK (number 1/12 → 1, title, artists, composers, lyrics) goes to that file's line, and only what belongs to the RECORD (album, album artist, year, genre) goes up. A rule the tests guard: what a person wrote is not overwritten by the next read.
The way back is obras.para_arquivo: "download with data" writes title, N/total, artists and album into a COPY (infra/metadados.py) — the file kept in the annex never changes, otherwise its SHA256 key would change and sync would see a different file.
feed_rss builds the podcast's RSS with public episodes only, with absolute links. Absolute links need the address the person came in through: _origem_publica (api/v3/saberes.py) uses BBX_ORIGEM_PUBLICA, or the X-Forwarded-Host the package's nginx passes on.
MusicBrainz is a request, not automatic: only with internet, BBX_METADADOS_REDE=on, and it fills in without erasing what exists.
The channel: programming as a function of the clock
The heart is in the docstring of services/canal.py: given a channel and an instant, any mucua and any browser reach the same answer — which file, from which saber, at which second. There is no stream server for the "radio synchronised over the web": the front asks GET /canais/{uuid}/agora, plays the file from the given position and asks again when it ends.
flowchart LR
G[data.grade<br/>blocks: days, start, end, sources] --> B{bloco_em t?}
B -- yes --> S[_seq_do_bloco<br/>sources → items, shuffle with fixed seed]
B -- no --> R[_seq_rotacao<br/>continuous loop since midnight]
S --> V[_com_vinhetas<br/>a jingle every N items]
R --> V
V --> C[_no_ciclo: item + position]
C --> A[agora: item, position, up next]
Pieces worth understanding:
itens_da_fontegoes down a root (dynamic roots and roots inside roots too, up toPROFUNDIDADE), a podcast (episodes in order), an album (tracks in order) or any saber (its attachments). Only what is public read as anonymous and has a measured duration gets in — and duration is only measured for files present on this mucua (repository.py). Radio plays audio and the sound of videos; TV, only video._sequenciais expensive (reads saberes, walks roots): it is cached forCACHE_SEGand is single-flight — with many people opening the page, one computes and the others wait for the same result.aleatorioshuffles with a fixed seed (channel + block + day): everyone hears the same "random" order.- The Up next list skips the silent gaps: it only counts real items (an old bug left the list short when the schedule had gaps).
diagnostico_fontes(routeGET /canais/fontes) tells, for each source, how many items and how much time play — or the reason they don't:sem_video,sem_duracao,nao_publico,nao_encontrado,vazio. TheSeletorFontesshows it on the chip; it is the answer to "why doesn't the TV play my album?".
The channel's identity is the channel's own attachments: data.logo (the cover; with data.marca_dagua, also the TV watermark, an ffmpeg overlay) and data.vinhetas, inserted every data.vinheta_a_cada items.
The transmitter: from the function to the stream
Only the channel's owner mucua (saber.mucua) broadcasts, and only with data.transmitir. A manager (iniciar_gerente, every INTERVALO) starts and stops one Transmissor per channel; a file lock guarantees a single one, even with the API and bbx canal transmitir running together.
- Radio: an encoder ffmpeg stays on all the time reading PCM; a loop feeds it the current file (
-re, from the right position) or silence. Since it never stops, listeners don't drop between tracks. HLS output always; Icecast if the mucua hasBBX_ICECAST_URL(the password lives on the mucua, never in the saber — the saber travels). - TV: raw video through the pipe would be too heavy; each file becomes HLS segments in the same list, with discontinuities, at
BBX_CANAL_TV_ALTURA/KBPS.
Don't get in the mucua's way. Playing a channel must not block sync or the database: the transmitter only READS (no write lock), ffmpeg runs under nice (BBX_CANAL_NICE) with few cores (BBX_CANAL_THREADS), and recording conversion runs at nice 19 with low best-effort disk priority.
Going live: three doors, one Sessao
flowchart LR
OBS[OBS / app] -- RTMP :1935 --> N[nginx-rtmp] -- on_publish --> API[/ao-vivo/rtmp/publicar/]
N --> F
SRT[SRT app] -- UDP 9000-9019 --> F
NAV[browser<br/>MediaRecorder] -- WebSocket --> Q[input queue<br/>+ writer thread] --> F
F[Sessao: one ffmpeg] --> H[channel HLS]
F --> GR[recording] --> SB[PRIVATE saber<br/>gravado_em → channel]
- RTMP: nginx receives and asks the API (
on_publish, only from 127.0.0.1) whether the key is valid.canal_da_chavefinds the channel; the key lives only in~/.baobaxia/canais/<uuid>/ao_vivo.chave— never in the saber. Editors can choose their own (validar_chave: 10–64 of[A-Za-z0-9_-], unique on the mucua → 409 if repeated). - SRT: an ffmpeg listens on one UDP port per channel, with the key as passphrase.
- Browser:
AoVivoNavegadorrecords withMediaRecorder(WebM H.264 preferably; VP8 on Firefox; MP4 last), a keyframe every 2 s, and sends the chunks over the/ao-vivo/navegador/{uuid}WebSocket. Switching camera midway uses a "stage" canvas: the recorder doesn't restart.
Lessons that cost dropped streams (read the git log):
- Never block the WebSocket handler. Waiting for ffmpeg inside it made uvicorn miss the ping and drop the connection (~35 s). Now
alimentaronly enqueues; a thread writes; afolga(BBX_AO_VIVO_FOLGA_MB) caps memory; andbbx apistarts withws_ping_timeout=60. - Don't re-encode when you can avoid it. Incoming H.264 (OBS, apps, Chrome/Edge/Safari) is copied to the HLS and to the recording (
copiar_video_ao_vivo); only VP8 is re-encoded. The quality the broadcaster chooses is what viewers get. - Double click created two sessions and the second killed the first:
Transmissor._ocupado()in the back and a disabled button in the front. - Silence means the end: with no data for
BBX_AO_VIVO_SILENCIO, the session ends and keeps what it recorded; whileencerrando,estado()already says "not live". - A full disk brings the mucua down: below
BBX_AO_VIVO_DISCO_MIN, going live doesn't even start.
The recording becomes a saber. guardar_gravacao: _converter only copies when the H.264 is up to 1.5× the target (~2000+160 kbps, about 1 GB/h); otherwise it redoes it at that size. It is born visibility: private (only its creator and the mucua's PAJÉs see it) with the gravado_em → channel relation (and gravacao_de on the other side), and, if the channel asks, as an episodio of the chosen podcast.
Turning it on and off: BBX_CANAIS
Radio/TV is optional. The postinst asks once ("Enable radio/TV?") and writes BBX_CANAIS:
| Value | Schedule | Live | nginx :1935 |
|---|---|---|---|
on (default) | the manager starts it | accepted | open (with libnginx-mod-rtmp) |
manual | only bbx canal transmitir | accepted | open |
off | no | refused (garantir → None, on_publish → 403, WebSocket closes with a reason) | closed |
The editor learns it from ao-vivo-config (canais_na_mucua: false) and says so. Tests run in manual: the manager doesn't start on its own, but going live is tested end to end.
In the front
GradeEditorbuilds blocks, rotation, time zone, identity;SeletorFontespicks sources withBuscaSaberes(the shared picker, with cover, type, date and galaxy) and shows each one's diagnosis.- On the channel page,
CanalAoVivoplays in sync (or the HLS, when there is a live broadcast), shows the 📻/📺 and 🌐 UTC±x chips, and the attachments stay folded. CanaisNoAris the On air now strip on the home page (GET /canais/no-ar).- The
Tocadorpublishes the work to the system's Media Session: cover, track, artists on the lock screen, next/previous buttons.
Practice
- Duration is only measured for what is here: for a mucua's TV, the archive must be present on it (
getbefore putting it in the schedule). - Touched the schedule? Run
test_canal_radio_tv.pyandtest_canal_identidade.py— they fix the clock and check the programme item by item. - Touched live?
test_ao_vivo_navegador.py(WebSocket with a real ffmpeg) andtest_ao_vivo_lento.py(slow mucua: the connection must not drop). - New variable? Put it in
docs/baobaxia-configuracao.md, under the right theme, marking whether it is essential, common or fine-tuning — and inREADME.Debian(section 4) only if an ordinary mucua needs to touch it.
To talk about
- The programme is deterministic so that every mucua hears the same thing without a central server. What other parts of Baobáxia could be "functions of the clock" instead of stored state?
- The recording is born private. Who should be able to publish it — the broadcaster, whoever looks after the channel, the circle? Today the code leaves it to the creator and the PAJÉs: is that enough?
- A conference (several people on video) can enter a channel as one more RTMP source. What changes — and what doesn't — if one day Baobáxia integrates a meeting room (LiveKit, OpenVidu)?
