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

  1. natures/schemas.py — the album, audiolivro, podcast, episodio and canal natures ("works of sound and image").
  2. services/obras.py — file metadata → saber (and back), a series' episodes, the RSS feed.
  3. enriquecimento/metadados_rede.py — MusicBrainz and Cover Art Archive.
  4. services/canal.py — the schedule becomes a FUNCTION of the clock (agora, programacao, diagnostico_fontes).
  5. services/transmissor.py — the programme becomes a stream (HLS, Icecast).
  6. services/ao_vivo.py — RTMP, SRT and browser; the Sessao; the recording.
  7. api/v3/canais.py — the routes; in bbx-make-deb, debian-tree/usr/share/baobaxia/nginx/baobaxia-rtmp.conf.
  8. In the front: GradeEditor, SeletorFontes, BuscaSaberes, CanalAoVivo, CanaisNoAr, AoVivoConexao, AoVivoNavegador, and the Tocador.
  9. 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:

NaturePartsWhere they live
album, audiolivrotracks / chaptersthe attachments, in data.ordem_conteudo order; each one's data in data.faixas[file]
podcastepisodeseach 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_fonte goes down a root (dynamic roots and roots inside roots too, up to PROFUNDIDADE), 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.
  • _sequencia is expensive (reads saberes, walks roots): it is cached for CACHE_SEG and is single-flight — with many people opening the page, one computes and the others wait for the same result.
  • aleatorio shuffles 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 (route GET /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. The SeletorFontes shows 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 has BBX_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_chave finds 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: AoVivoNavegador records with MediaRecorder (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 alimentar only enqueues; a thread writes; a folga (BBX_AO_VIVO_FOLGA_MB) caps memory; and bbx api starts with ws_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; while encerrando, 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:

ValueScheduleLivenginx :1935
on (default)the manager starts itacceptedopen (with libnginx-mod-rtmp)
manualonly bbx canal transmitiracceptedopen
offnorefused (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

  • GradeEditor builds blocks, rotation, time zone, identity; SeletorFontes picks sources with BuscaSaberes (the shared picker, with cover, type, date and galaxy) and shows each one's diagnosis.
  • On the channel page, CanalAoVivo plays in sync (or the HLS, when there is a live broadcast), shows the 📻/📺 and 🌐 UTC±x chips, and the attachments stay folded.
  • CanaisNoAr is the On air now strip on the home page (GET /canais/no-ar).
  • The Tocador publishes 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 (get before putting it in the schedule).
  • Touched the schedule? Run test_canal_radio_tv.py and test_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) and test_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 in README.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)?

vedi questo capitolo come sapere