Parte III — Ramas, savia y semillas
12 · Los tambores: obras de sonido, canales y en vivo
El tambor es la red más antigua: un toque en una aldea se vuelve respuesta en la otra. Aquí Baobáxia gana voz — discos y pódcasts con orden y autoría, radios y TVs que tocan el acervo según la parrilla, y el en vivo que entra en medio y después se vuelve saber.
Archivos para leer
natures/schemas.py— las naturesalbum,audiolivro,podcast,episodioycanal("obras de sonido e imagen").services/obras.py— metadatos de los archivos → saber (y de vuelta), los episodios de una serie, el feed RSS.enriquecimento/metadados_rede.py— MusicBrainz y Cover Art Archive.services/canal.py— la parrilla se vuelve una FUNCIÓN del reloj (agora,programacao,diagnostico_fontes).services/transmissor.py— la programación se vuelve stream (HLS, Icecast).services/ao_vivo.py— RTMP, SRT y navegador; laSessao; la grabación.api/v3/canais.py— las rutas; enbbx-make-deb,debian-tree/usr/share/baobaxia/nginx/baobaxia-rtmp.conf.- En el front:
GradeEditor,SeletorFontes,BuscaSaberes,CanalAoVivo,CanaisNoAr,AoVivoConexao,AoVivoNavegador, y elTocador. - Pruebas:
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: el orden y la autoría viven en el saber
Un midia con diez mp3 no sabe que es un disco. Las natures de obra sí:
| Nature | Partes | Dónde quedan |
|---|---|---|
album, audiolivro | pistas / capítulos | los adjuntos, en el orden de data.ordem_conteudo; los datos de cada uno en data.faixas[archivo] |
podcast | episodios | cada episodio es un saber con data.serie → el pódcast, numero, temporada |
obras.do_arquivo mapea los metadatos de dentro del archivo (ID3, MP4, Vorbis): en un multimedia, lo que dice el archivo va al saber entero; en el álbum, lo que es de la PISTA (número 1/12 → 1, título, artistas, composición, letra) va a la línea de ese archivo, y solo lo que es del DISCO (álbum, artista del álbum, año, género) sube. Regla que guardan las pruebas: lo que la persona escribió no se sobrescribe en la lectura siguiente.
El camino de vuelta es obras.para_arquivo: "descargar con datos" graba título, N/total, artistas y álbum en una COPIA (infra/metadados.py) — el archivo guardado en el annex nunca cambia; si no, cambiaría su clave SHA256 y el sync vería otro archivo.
feed_rss arma el RSS del pódcast solo con episodios públicos, con enlaces absolutos. Los enlaces absolutos necesitan la dirección por la que llegó la persona: _origem_publica (api/v3/saberes.py) usa BBX_ORIGEM_PUBLICA, o el X-Forwarded-Host que pasa el nginx del paquete.
MusicBrainz es un pedido, no algo automático: solo con internet, BBX_METADADOS_REDE=on, y completa sin borrar lo que existe.
El canal: la programación como función del reloj
El corazón está en el docstring de services/canal.py: dado un canal y un instante, cualquier mucua y cualquier navegador llegan a la misma respuesta — qué archivo, de qué saber, en qué segundo. No hay servidor de stream para la "radio sincronizada por la web": el front pregunta GET /canais/{uuid}/agora, toca el archivo desde la posición indicada y pregunta de nuevo cuando termina.
flowchart LR
G[data.grade<br/>bloques: días, inicio, fin, fuentes] --> B{bloco_em t?}
B -- sí --> S[_seq_do_bloco<br/>fuentes → ítems, mezclar con semilla fija]
B -- no --> R[_seq_rotacao<br/>vuelta continua desde la medianoche]
S --> V[_com_vinhetas<br/>una cortina cada N ítems]
R --> V
V --> C[_no_ciclo: ítem + posición]
C --> A[agora: ítem, posición, a continuación]
Piezas que vale entender:
itens_da_fontebaja por una raíz (también dinámica y raíz dentro de raíz, hastaPROFUNDIDADE), un pódcast (episodios en orden), un álbum (pistas en orden) o cualquier saber (sus adjuntos). Solo entra lo que es público leído como anónimo y tiene duración medida — y la duración solo se mide para archivos presentes en esta mucua (repository.py). La radio toca audio y el sonido de los videos; la TV, solo video._sequenciaes cara (lee saberes, recorre raíces): queda en caché porCACHE_SEGy es single-flight — con mucha gente abriendo la página, uno calcula y los demás esperan el mismo resultado.aleatoriomezcla con semilla fija (canal + bloque + día): todo el mundo oye el mismo orden "aleatorio".- La lista A continuación salta los huecos de silencio: solo cuenta ítems de verdad (un bug antiguo dejaba la lista corta cuando la parrilla tenía intervalos).
diagnostico_fontes(rutaGET /canais/fontes) dice, para cada fuente, cuántos ítems y cuánto tiempo suenan — o el motivo de no sonar:sem_video,sem_duracao,nao_publico,nao_encontrado,vazio. ElSeletorFonteslo muestra en el chip; es la respuesta a "¿por qué la TV no toca mi álbum?".
La identidad del canal son los adjuntos del propio canal: data.logo (la portada; con data.marca_dagua, también la marca de agua de la TV, un overlay de ffmpeg) y data.vinhetas, insertadas cada data.vinheta_a_cada ítems.
El transmisor: de la función al stream
Solo la mucua dueña del canal (saber.mucua) transmite, y solo con data.transmitir. Un gestor (iniciar_gerente, cada INTERVALO) enciende y apaga un Transmissor por canal; un bloqueo de archivo garantiza uno solo, incluso con la API y bbx canal transmitir corriendo juntos.
- Radio: un ffmpeg codificador queda encendido todo el tiempo leyendo PCM; un bucle lo alimenta con el archivo de turno (
-re, desde la posición justa) o con silencio. Como nunca para, quien escucha no se cae entre pistas. Salida HLS siempre; Icecast si la mucua tieneBBX_ICECAST_URL(la contraseña vive en la mucua, nunca en el saber — el saber viaja). - TV: video crudo por el tubo sería demasiado pesado; cada archivo se vuelve segmentos HLS en la misma lista, con discontinuidad, en
BBX_CANAL_TV_ALTURA/KBPS.
No estorbar a la mucua. Tocar un canal no puede trabar el sync ni la base de datos: el transmisor solo LEE (sin bloqueo de escritura), el ffmpeg corre con nice (BBX_CANAL_NICE) y pocos núcleos (BBX_CANAL_THREADS), y la conversión de la grabación corre en nice 19 con disco best-effort bajo.
El en vivo: tres puertas, una 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[cola de entrada<br/>+ hilo escritor] --> F
F[Sessao: un ffmpeg] --> H[HLS del canal]
F --> GR[grabación] --> SB[saber PRIVADO<br/>gravado_em → canal]
- RTMP: el nginx recibe y pregunta a la API (
on_publish, solo desde 127.0.0.1) si la clave vale.canal_da_chaveencuentra el canal; la clave vive solo en~/.baobaxia/canais/<uuid>/ao_vivo.chave— nunca en el saber. Quien edita puede elegir la suya (validar_chave: 10–64 de[A-Za-z0-9_-], única en la mucua → 409 si se repite). - SRT: un ffmpeg escucha en un puerto UDP por canal, con la clave como passphrase.
- Navegador:
AoVivoNavegadorgraba conMediaRecorder(WebM H.264 de preferencia; VP8 en Firefox; MP4 al final), un fotograma clave cada 2 s, y manda los trozos por el WebSocket/ao-vivo/navegador/{uuid}. Cambiar de cámara en medio usa un canvas "escenario": el grabador no se reinicia.
Lecciones que costaron caídas (lee el git log):
- Nunca bloquear el handler del WebSocket. Esperar el ffmpeg dentro de él hacía que uvicorn perdiera el ping y tirara la conexión (~35 s). Ahora
alimentarsolo encola; un hilo escribe; unafolga(BBX_AO_VIVO_FOLGA_MB) limita la memoria; ybbx apiarranca conws_ping_timeout=60. - Sin reconvertir cuando se puede. El H.264 que llega (OBS, apps, Chrome/Edge/Safari) se copia al HLS y a la grabación (
copiar_video_ao_vivo); solo VP8 se reconvierte. La calidad que elige quien transmite es la de quien mira. - El doble clic creaba dos sesiones y la segunda mataba a la primera:
Transmissor._ocupado()en el back y el botón desactivado en el front. - Silencio es fin: sin datos durante
BBX_AO_VIVO_SILENCIO, la sesión termina y guarda lo grabado; durante elencerrando,estado()ya dice "no está en vivo". - El disco lleno tumba la mucua: por debajo de
BBX_AO_VIVO_DISCO_MIN, el en vivo ni empieza.
La grabación se vuelve saber. guardar_gravacao: _converter solo copia cuando el H.264 está hasta 1,5× el objetivo (~2000+160 kbps, cerca de 1 GB/h); si no, lo rehace en ese tamaño. Nace visibility: private (solo quien la creó y los PAJÉs de la mucua la ven) con la relación gravado_em → canal (y gravacao_de del otro lado), y, si el canal lo pide, como episodio del pódcast elegido.
Encender y apagar: BBX_CANAIS
La radio/TV es opcional. El postinst pregunta una vez ("¿Activar radio/TV?") y graba BBX_CANAIS:
| Valor | Parrilla | En vivo | nginx :1935 |
|---|---|---|---|
on (por defecto) | la enciende el gestor | aceptado | abierto (con libnginx-mod-rtmp) |
manual | solo bbx canal transmitir | aceptado | abierto |
off | no | rechazado (garantir → None, on_publish → 403, el WebSocket cierra con motivo) | cerrado |
El editor lo sabe por ao-vivo-config (canais_na_mucua: false) y avisa. Las pruebas corren en manual: el gestor no se enciende solo, pero el en vivo se prueba de punta a punta.
En el front
GradeEditorarma bloques, rotación, huso horario, identidad;SeletorFonteselige las fuentes con elBuscaSaberes(el buscador común, con portada, tipo, fecha y galaxia) y muestra el diagnóstico de cada una.- En la página del canal,
CanalAoVivotoca sincronizado (o el HLS, cuando hay en vivo), muestra los chips 📻/📺 y 🌐 UTC±x, y los adjuntos quedan recogidos. CanaisNoAres la franja Al aire ahora de la página de inicio (GET /canais/no-ar).- El
Tocadorpublica la obra en la Media Session del sistema: portada, pista, artistas en la pantalla bloqueada, botones de siguiente/anterior.
Prácticas
- La duración solo se mide de lo que está aquí: para la TV de una mucua, el acervo tiene que estar presente en ella (
getantes de ponerlo en la parrilla). - ¿Tocaste la parrilla? Corre
test_canal_radio_tv.pyytest_canal_identidade.py— fijan el reloj y revisan la programación ítem por ítem. - ¿Tocaste el en vivo?
test_ao_vivo_navegador.py(WebSocket con ffmpeg de verdad) ytest_ao_vivo_lento.py(mucua lenta: la conexión no puede caerse). - ¿Variable nueva? Ponla en
docs/baobaxia-configuracao.md, en el tema correcto, marcando si es esencial, común o ajuste fino — y en elREADME.Debian(sección 4) solo si una mucua común necesita tocarla.
Para conversar
- La programación es determinista para que todas las mucuas oigan lo mismo sin servidor central. ¿Qué otras partes de Baobáxia podrían ser "funciones del reloj" en vez de estado guardado?
- La grabación nace privada. ¿Quién debería poder publicarla — quien transmitió, quien cuida el canal, la rueda? Hoy el código lo deja a quien la creó y a los PAJÉs: ¿basta?
- Una conferencia (varias personas en video) puede entrar en un canal como una fuente RTMP más. ¿Qué cambia — y qué no — si un día Baobáxia integra una sala de conversación (LiveKit, OpenVidu)?
