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

  1. natures/schemas.py — las natures album, audiolivro, podcast, episodio y canal ("obras de sonido e imagen").
  2. services/obras.py — metadatos de los archivos → saber (y de vuelta), los episodios de una serie, el feed RSS.
  3. enriquecimento/metadados_rede.py — MusicBrainz y Cover Art Archive.
  4. services/canal.py — la parrilla se vuelve una FUNCIÓN del reloj (agora, programacao, diagnostico_fontes).
  5. services/transmissor.py — la programación se vuelve stream (HLS, Icecast).
  6. services/ao_vivo.py — RTMP, SRT y navegador; la Sessao; la grabación.
  7. api/v3/canais.py — las rutas; en bbx-make-deb, debian-tree/usr/share/baobaxia/nginx/baobaxia-rtmp.conf.
  8. En el front: GradeEditor, SeletorFontes, BuscaSaberes, CanalAoVivo, CanaisNoAr, AoVivoConexao, AoVivoNavegador, y el Tocador.
  9. 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í:

NaturePartesDónde quedan
album, audiolivropistas / capítuloslos adjuntos, en el orden de data.ordem_conteudo; los datos de cada uno en data.faixas[archivo]
podcastepisodioscada 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_fonte baja por una raíz (también dinámica y raíz dentro de raíz, hasta PROFUNDIDADE), 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.
  • _sequencia es cara (lee saberes, recorre raíces): queda en caché por CACHE_SEG y es single-flight — con mucha gente abriendo la página, uno calcula y los demás esperan el mismo resultado.
  • aleatorio mezcla 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 (ruta GET /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. El SeletorFontes lo 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 tiene BBX_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_chave encuentra 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: AoVivoNavegador graba con MediaRecorder (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 alimentar solo encola; un hilo escribe; una folga (BBX_AO_VIVO_FOLGA_MB) limita la memoria; y bbx api arranca con ws_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 el encerrando, 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:

ValorParrillaEn vivonginx :1935
on (por defecto)la enciende el gestoraceptadoabierto (con libnginx-mod-rtmp)
manualsolo bbx canal transmitiraceptadoabierto
offnorechazado (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

  • GradeEditor arma bloques, rotación, huso horario, identidad; SeletorFontes elige las fuentes con el BuscaSaberes (el buscador común, con portada, tipo, fecha y galaxia) y muestra el diagnóstico de cada una.
  • En la página del canal, CanalAoVivo toca sincronizado (o el HLS, cuando hay en vivo), muestra los chips 📻/📺 y 🌐 UTC±x, y los adjuntos quedan recogidos.
  • CanaisNoAr es la franja Al aire ahora de la página de inicio (GET /canais/no-ar).
  • El Tocador publica 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 (get antes de ponerlo en la parrilla).
  • ¿Tocaste la parrilla? Corre test_canal_radio_tv.py y test_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) y test_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 el README.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)?

vedi questo capitolo come sapere