🎥 ScreenCast: el stream SE CREA — nodo cosmic-screencast vivo en pipewire

La receta de la sonda (b3:af49d32d) y lo que midió. Es la única receta de la campaña
cuyo producto no es una pieza del escritorio sino un instrumento para medirlo: entra
a la imagen porque un handshake de portal sólo se puede ejercer DESDE la sesión, con
bus y compositor vivos, y sale ESTÁTICA (0 NEEDED) porque un instrumento no debe
depender de aquello que mide.

  [1/4] CreateSession  → Response 0, session_handle                    ✓
  [2/4] SelectSources  → Response 0                                   ✓
  [3/4] Start          → el backend ABRE "Share your screen", con
                         miniatura EN VIVO del framebuffer y el output
                         Virtual-1; se elige, se pulsa Share…
                         y NO llega Response en 60s                    ✗

El log del backend da la línea exacta:
  screencast_thread: state-changed 'Connecting' -> 'Paused'

Y `pw-cli ls Node` durante la espera da el veredicto INDEPENDIENTE:
  node.name = "cosmic-screencast"   media.class = "Video/Source"

EL NODO DE VIDEO EXISTE EN EL GRAFO DE PIPEWIRE. La cadena entera —cliente →
frontend → backend → compositor → demonio— funciona hasta crear y negociar el stream.
Lo único que no ocurre es el Response de vuelta tras quedar en `Paused`. Eso es un
lugar muy distinto del de esta mañana ("no hay demonio con quien negociar").

Sospecha para el próximo paso, y es HIPÓTESIS no medición: falta `wireplumber`. Sin
gestor de sesión nadie mueve el nodo de Paused a Streaming y el backend parece
esperarlo. Construirlo la confirma o la mata.

Gotchas que costaron corridas y quedan escritos:
 · matar el backend se lleva puesto al frontend (ambos pierden dueño del bus);
 · el lanzador busca por NOMBRE VISIBLE: `cosmic-term` no matchea, `Terminal` sí —
   escribir el nombre del binario abre otra app;
 · `| head -N` bufferiza y deja la terminal en blanco: parece colgada y está esperando;
 · pkg-config OMITE los -L de dirs "estándar" y zig cc cross NO los busca ⇒
   `-ldbus-1` pelado da "unable to find static system library", que suena a librería
   faltante cuando lo que falta es la ruta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-05 10:28:14 -04:00
co-authored by Claude Opus 5
parent ea210ba3cd
commit ee037912a9
6 changed files with 185 additions and 8 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+90 -8
View File
@@ -1009,14 +1009,95 @@ mutilado da 130).
**Lo que sigue faltando, y son dos cosas separadas:**
- **El handshake de ScreenCast de punta a punta** necesita un cliente `ashpd` **persistente**, y no
hay atajo: el portal asocia la sesión al *sender* de D-Bus, así que `CreateSession` con un
`dbus-send` y `SelectSources` con otro se rechazan por venir de nombres únicos distintos. Un
cliente de prueba es una receta nueva, no un one-liner.
- **El handshake de ScreenCast de punta a punta** necesita un cliente **persistente** — resuelto
abajo con `portal-probe`.
- **`wireplumber`**, que es lo que separa «hay stream de video» de «hay audio». Sin gestor de sesión
pipewire arranca y sirve, pero no descubre tarjetas ni enruta. Para ScreenCast alcanza; para sonido
no. Ni siquiera está construido.
## 🔌 portal-probe: la SONDA, y hasta dónde llega ScreenCast de verdad (2026-08-05)
`recipes/incoming-cosmic/portal-probe.toml` → **`b3:af49d32d`**. La única receta de la campaña cuyo
producto no es una pieza del escritorio sino **un instrumento para medirlo**.
### Por qué no alcanzaba `dbus-send`, y por qué tampoco hacía falta `ashpd`
Los portales no son RPC: son **asíncronos en dos tiempos**. Cada método devuelve al instante un
object path de `Request`, y el resultado llega DESPUÉS como señal `Request.Response` sobre ese path.
`dbus-send` ya salió para entonces. Y hay un segundo muro, más duro: **el portal ata la sesión al
SENDER**, así que `CreateSession` con un `dbus-send` y `SelectSources` con otro se rechazan por venir
de nombres únicos distintos. Hace falta UNA conexión viva durante todo el intercambio.
`ashpd` era el camino obvio, pero el árbol fuente de una receta de código propio es el repo del
**propio hammer** (no hay modo `path` en `[source]`: sólo git y tarball) ⇒ habría sumado ~150 crates
al `Cargo.lock` de la herramienta de build por una sonda de diagnóstico. En C sobre `libdbus` cuesta
**cero deps nuevas**, sale **estática (0 NEEDED)** —un instrumento no debe depender de lo que mide— y
sobre todo: **el protocolo se ve**. Una sonda cuyo valor es documentar un handshake no debería
esconderlo tras una biblioteca que lo abstrae.
Detalle que la especificación regala y hay que usar: **el path del `Request` se PREDICE**
(`/…/request/<sender>/<token>`, con `sender` = nombre único sin `:` y con `.`→`_`). Existe justo para
poder suscribirse ANTES de llamar; esperar al path devuelto para recién ahí hacer `AddMatch` es una
carrera real, no teórica.
### Lo medido: el stream SE CREA, y el `Response` es lo único que falta
```
portal-probe version ScreenCast v5 · Screenshot v2 · FileChooser v3 · Settings v2
Access y RemoteDesktop AUSENTES
portal-probe screencast
[1/4] CreateSession → Response 0, session_handle = /…/session/1_108/hammersess ✓
[2/4] SelectSources → Response 0 ✓
[3/4] Start → el backend ABRE su diálogo «Share your screen», con
miniatura EN VIVO del framebuffer y el output `Virtual-1`;
se elige, se pulsa Share… y NO llega Response en 60s ✗
```
El log del backend (`RUST_LOG=debug`) da la línea exacta:
```
xdg_desktop_portal_cosmic::screencast_thread: state-changed 'Connecting' -> 'Paused'
```
Y `pw-cli ls Node` durante la espera da el veredicto independiente:
```
node.name = "cosmic-screencast"
media.class = "Video/Source"
```
**El nodo de video EXISTE en el grafo de pipewire.** O sea: la cadena entera —cliente → frontend →
backend → compositor → demonio de pipewire— funciona hasta **crear y negociar el stream**. Lo único
que no ocurre es que el backend emita el `Response` de vuelta al cliente tras quedar en `Paused`.
Ese es un lugar muy distinto del que ocupaba el frente esta mañana («no hay demonio con quien
negociar»). El siguiente paso es el `Paused → Streaming`, y la sospecha razonable es
**`wireplumber`**: sin gestor de sesión nadie mueve el nodo de `Paused`, y el backend parece
esperarlo. Es una hipótesis, no una medición — construir `wireplumber` la confirma o la mata.
### Dos gotchas que costaron corridas
- **`NameHasOwner` no activa nada.** Los portales son servicios ACTIVABLES: el bus los arranca al
pedirlos. La primera versión de la sonda rechazaba con «no hay frontend de portal en el bus» sobre
un sistema donde el portal andaba perfecto, sólo que dormido. Se arregló con `StartServiceByName`
(commit `ea210ba`) — y distingue «arrancado ahora» de «ya estaba corriendo», que no es lo mismo
cuando lo que se investiga es una carrera.
- **Matar el backend se lleva puesto al frontend.** `kill` sobre `xdg-desktop-portal-cosmic` dejó
también sin dueño a `org.freedesktop.portal.Desktop`. Para diagnosticar con log hay que relanzar el
backend Y forzar la reactivación del frontend.
- **El lanzador busca por NOMBRE VISIBLE, no por binario**: `cosmic-term` no matchea nada,
`Terminal` abre COSMIC Terminal. Escribir el nombre del ejecutable abre otra cosa (settings).
- Y uno de shell, al automatizar por QMP: **`| head -N` bufferiza** y deja la terminal en blanco hasta
que el programa termina. Parece que la sonda colgó cuando en realidad está esperando.
### El `-L/usr/lib` que pkg-config no da
`pkg-config` **omite los `-L` de directorios que considera estándar del sistema**, asumiendo que el
linker ya los busca. Cierto para un `cc` nativo; **falso para `zig cc` cruzando a
`x86_64-linux-musl`**, que trae sus propias rutas. Resultado: `--libs --static` devuelve `-ldbus-1`
pelado y zig responde `unable to find static system library 'dbus-1' … searched paths: none` — un
error que suena a «falta la librería» cuando lo que falta es la ruta.
**Y de paso se corrigió un comentario desactualizado de `cosmic-start`** que decía que
`cosmic-settings-daemon` «todavía no se puede construir acá (arrastra pipewire)». Es falso hace
tiempo: ese daemon está sellado y corre. Un comentario viejo manda a buscar un bloqueo que ya no
@@ -1024,10 +1105,11 @@ existe, y eso cuesta igual que un bug.
## Lo que falta, en orden
1. ~~Arrancar el demonio de pipewire desde `cosmic-start`~~ **✅ hecho (2026-08-05)**, y el portal ya
figura como cliente suyo. Queda el **cliente `ashpd` persistente** para ejercer el handshake de
ScreenCast y, de paso, capturar la URI del `FileChooser` que `dbus-send` no puede ver — es una
receta, no un one-liner, por lo del *sender* de D-Bus explicado arriba. Y `wireplumber` para audio.
1. ~~Arrancar el demonio de pipewire~~ ✅ y ~~el cliente persistente~~ ✅ (`portal-probe`, `b3:af49d32d`).
**Lo que queda es `wireplumber`**: el stream de ScreenCast se crea y llega a `Paused`, con nodo
`cosmic-screencast` visible en pipewire, pero nadie lo pasa a `Streaming` y el backend no emite el
`Response`. La hipótesis es que falta el gestor de sesión — construirlo la confirma o la mata.
Falta también correr `portal-probe filechooser`, que ya puede capturar la URI.
2. **Decidir `gvfs`** (ver la corrección de arriba: las piezas ya están selladas, lo que falta es la
decisión sobre la segunda glib). Destrabaría los montajes remotos de cosmic-files y su applet.
3. **Enchufar la tienda a `.swm`**: servir `org.freedesktop.PackageKit` y encender la feature
+4
View File
@@ -133,6 +133,10 @@ paquetes = [
# Access/FileChooser/Screenshot/Settings/ScreenCast. Sin ellos no hay captura, ni selector de
# ficheros de portal, ni compartir pantalla.
"xdg-desktop-portal", "xdg-desktop-portal-cosmic",
# Y la SONDA con que se mide esa cadena. No es escritorio: es el instrumento. Se lista como raíz
# porque nadie la declara como dep y sin ella el perfil no incluiría con qué verificarse a sí
# mismo — un handshake de portal sólo se puede ejercer DESDE la sesión, con bus y compositor vivos.
"portal-probe",
# Datos, no binarios: el tema de iconos (y el hicolor del que hereda), las tablas de xkb y una
# tipografía. Ninguno es dep de build de nadie y sin ellos el escritorio arranca roto — sin puntero,
# sin teclado o con tofu.
+86
View File
@@ -0,0 +1,86 @@
# portal-probe — la SONDA del portal XDG. La única receta de la campaña cuyo producto no es una
# pieza del escritorio sino un INSTRUMENTO para medirlo.
#
# ── QUÉ MIDE, Y POR QUÉ NINGUNA HERRAMIENTA EXISTENTE SIRVE ─────────────────────────────────────
# Los portales no son RPC: son un protocolo asíncrono en dos tiempos. Cada método devuelve al
# instante un object path de `Request`, y el resultado de verdad llega DESPUÉS como señal
# `org.freedesktop.portal.Request.Response` sobre ese path. `dbus-send` ya salió para entonces ⇒
# devuelve el path y nunca el resultado. Eso es lo que impidió capturar la URI del `FileChooser`.
#
# Y hay un segundo muro, más duro: **el portal ata la sesión al SENDER**. `CreateSession` con un
# `dbus-send` y `SelectSources` con otro llegan desde nombres únicos DISTINTOS y el portal rechaza
# el segundo. No hay forma de encadenar el handshake desde el shell; hace falta UNA conexión que
# viva todo el intercambio. Por eso esto es un binario y no un script.
#
# ── POR QUÉ EN C Y NO CON `ashpd` ───────────────────────────────────────────────────────────────
# `ashpd` es el cliente de portales del ecosistema Rust —es lo que usan `cosmic-screenshot` y
# `cosmic-files`— y era el camino obvio. Pero el árbol fuente de esta receta es el repo del PROPIO
# hammer (no hay modo `path` en `[source]`: sólo git y tarball), así que usar ashpd significaría
# sumar ~150 crates al `Cargo.lock` de la herramienta de build por una sonda de diagnóstico.
#
# `libdbus` ya está en el corpus, cuesta cero deps nuevas, y `dbus` provee `libdbus-1.a` ⇒ la sonda
# sale **estática, sin un solo NEEDED**, que es justo lo que uno quiere de un instrumento: que no
# dependa de nada de aquello que va a medir. Y sobre todo: en C el protocolo SE VE. Una sonda cuyo
# valor es documentar un handshake no debería esconderlo tras una biblioteca que lo abstrae.
#
# ── EL VENDOREO QUE SE PAGA IGUAL ───────────────────────────────────────────────────────────────
# El árbol fuente es el repo hammer entero, cuya raíz tiene `Cargo.toml` ⇒ `detect_build_system`
# dice Cargo y el fetch vendorea el workspace. No se puede apagar (`cargo_vendor` sólo FUERZA, no
# desactiva). Es un costo de tiempo en el fetch, no de corrección: la fase `compile` de acá abajo
# ignora cargo por completo y llama a `zig cc` sobre un único `.c`. Mismo peaje que paga `hammerd`.
name = "portal-probe"
version = "0.1.0"
[source]
# El repo del propio hammer. El `commit` es el identificador inmutable (la URL es sólo locator y no
# entra al hash). Subir el commit cuando la sonda avance — y sólo entonces: fijar el commit es lo
# que hace que un instrumento de medición sea él mismo reproducible.
repo = "ssh://gitea@git.gioser.net:2345/sergio/hammer.git"
commit = "ea210ba3cd40f20325eae1b7023db280814e734d"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
# Estático de verdad, y acá SÍ es la decisión correcta en vez de la coherencia con la suite: el
# resto de los clientes van dinámicos porque `wayland-client` hace `dlopen` (ver el runbook). Esta
# sonda no toca wayland, y un instrumento que arrastra NEEDED puede fallar por una razón que no es
# la que está midiendo.
link = "static"
[build.phases]
# Sin configure: es un fichero.
configure = "true"
# `zig cc` directo. Los flags de dbus salen de su `.pc`, que vive en el artefacto del store y el
# sandbox proyecta en /usr — de ahí el PKG_CONFIG_PATH. Los dos `-I` de dbus son SIEMPRE dos y no
# uno: `dbus-arch-deps.h` es generado y vive bajo `lib/dbus-1.0/include`, no junto al resto de los
# headers. Un `-I` solo compila hasta que algo incluye `dbus.h` de verdad.
#
# ⚠ EL `-L/usr/lib` HAY QUE PONERLO A MANO, y el motivo es un choque de supuestos entre dos
# herramientas: **pkg-config OMITE los `-L` de directorios que considera estándar del sistema**
# (`/usr/lib` lo es), asumiendo que el linker ya los busca. Cierto para un `cc` nativo; FALSO para
# `zig cc` cruzando a `x86_64-linux-musl`, que trae sus propias rutas y no da nada por sentado.
# Resultado: `--libs --static` devuelve `-ldbus-1` pelado y zig responde
# `unable to find static system library 'dbus-1' … searched paths: none` — un error que suena a
# «falta la librería» cuando la librería está ahí y lo que falta es la ruta.
compile = '''
export PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig
CF=$(pkg-config --cflags dbus-1)
LF=$(pkg-config --libs --static dbus-1)
echo "cflags: $CF"
echo "libs : $LF"
zig cc -target x86_64-linux-musl -static -O2 -Wall -Wextra \
-o portal-probe tools/portal-probe/portal-probe.c $CF -L/usr/lib $LF
'''
install = '''
mkdir -p /out/usr/bin
cp portal-probe /out/usr/bin/portal-probe
chmod 755 /out/usr/bin/portal-probe
'''
[deps]
# `dbus` por la librería y los headers; `pkgconf` para que el `.pc` se pueda leer. Nada más: la
# sonda no dibuja, no abre ventana y no habla wayland — igual que `cosmic-screenshot`, que también
# es un cliente puro de portal.
build = ["dbus", "pkgconf"]
+5
View File
@@ -79,6 +79,11 @@ else
# por D-Bus en runtime, así que el grafo no los ve — igual que los componentes de la sesión.
recipes/incoming-cosmic/xdg-desktop-portal.toml
recipes/incoming-cosmic/xdg-desktop-portal-cosmic.toml
# …y la SONDA con la que se mide esa cadena. No es parte del escritorio: es el instrumento.
# Entra a la imagen a propósito —un handshake de portal sólo se puede ejercer DESDE la sesión,
# con el bus y el compositor vivos— y sale estática, sin un NEEDED, justamente para que nunca
# falle por algo del sistema que está midiendo.
recipes/incoming-cosmic/portal-probe.toml
recipes/incoming-cosmic/cosmic-app-library.toml
recipes/incoming-cosmic/cosmic-workspaces-epoch.toml
# Ni binario ni librería: DATOS. El tema de iconos de COSMIC — sin él el escritorio no se ve