atuq: AV1 no reproducía, y la causa era UN elemento de LD_LIBRARY_PATH
Gecko SE COME EL PRIMER ELEMENTO de `LD_LIBRARY_PATH` al lanzar el proceso RDD
—el que decodifica vídeo—. El lanzador ponía `/usr/lib/atuq` una sola vez, o sea
primero, así que el RDD arrancaba sin él, no encontraba `libmozavcodec.so` /
`libmozavutil.so` (el ffvpx bundleado, donde vive dav1d) y anotaba:
PlatformDecoderModule FFVPX: Link result: NoProvidedLib
El navegador NO falla ahí: se cae al ffmpeg del sistema, que cubre
H.264/AAC/VP8/VP9/MP3/FLAC/Opus. Pero AV1 por software NO lo cubre —el
decodificador `av1` de ffmpeg es sólo hwaccel y nuestra receta va sin dav1d—, así
que un vídeo AV1 se quedaba en `readyState=1` para siempre, sin un error, sin un
NEEDED faltante y sin una cadena ausente. Media función apagada en silencio, que
es la forma de fallo de esta casa.
Tres corridas que sólo cambian esa variable, con el mismo artefacto:
LD=/usr/lib/atuq:/usr/lib:/lib → RDD NoProvidedLib AV1 ✗
LD=/usr/lib/atuq:/usr/lib/atuq:/usr/lib → RDD Success AV1 ✓
LD=/relleno:/usr/lib/atuq:/usr/lib → RDD Success AV1 ✓
El arreglo es repetir el appdir. Feo y correcto mientras no haya `patchelf` en el
corpus para grabar `RUNPATH=$ORIGIN`, que borraría la variable entera.
Y el guardián que sale de este punto ciego, `scripts/test-atuq-codecs.sh`: abre
cinco muestras versionadas en `scripts/fixtures/codecs/` y mira si `currentTime`
AVANZA — «se creó el decodificador» no es «decodifica». El veredicto sale por
`dump()` al stdout, así que no necesita ni red ni servidor, y corre headless para
que sirva en el worker. Con `--negative-control` se saltea el lanzador y EXIGE
que AV1 falle: probado en los dos sentidos, 5/5 y control ✓.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
This commit is contained in:
+21
-1
@@ -19,6 +19,26 @@
|
||||
# `exec` y no una llamada normal: así el proceso que queda es el motor, `/proc/self/exe` apunta a
|
||||
# `/usr/lib/atuq/atuq` y Gecko resuelve su appdir correctamente. Con un `sh` de por medio quedaría
|
||||
# un proceso zombi de más y el nombre en `ps` sería el del script.
|
||||
LD_LIBRARY_PATH="/usr/lib/atuq${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
# ── ⚠ EL DIRECTORIO VA DOS VECES, Y NO ES UN DEDAZO (2026-09-07) ──────────────────────────────
|
||||
# Gecko SE COME EL PRIMER ELEMENTO de `LD_LIBRARY_PATH` al lanzar el proceso RDD (el que decodifica
|
||||
# vídeo). Con el appdir una sola vez —o sea, primero— el RDD arranca SIN él, no encuentra
|
||||
# `libmozavcodec.so`/`libmozavutil.so` (el ffvpx bundleado, que es donde vive dav1d) y registra
|
||||
#
|
||||
# PlatformDecoderModule FFVPX: Link result: NoProvidedLib
|
||||
#
|
||||
# El navegador NO falla: se cae al ffmpeg del sistema, que cubre H.264/AAC/VP8/VP9/MP3/FLAC/Opus…
|
||||
# pero **NO AV1 por software** —el decodificador `av1` de ffmpeg es sólo hwaccel y nuestra receta va
|
||||
# sin dav1d—, así que un vídeo AV1 se queda en `readyState=1` para siempre. Es la forma de fallo de
|
||||
# esta casa: media función apagada, cero mensajes de error.
|
||||
#
|
||||
# Medido con tres corridas que sólo cambian esta variable (matriz en `scripts/test-atuq-codecs.sh`):
|
||||
#
|
||||
# LD=/usr/lib/atuq:/usr/lib:/lib → RDD NoProvidedLib AV1 ✗
|
||||
# LD=/usr/lib/atuq:/usr/lib/atuq:/usr/lib → RDD Success AV1 ✓
|
||||
# LD=/relleno:/usr/lib/atuq:/usr/lib → RDD Success AV1 ✓ (el relleno se lo come)
|
||||
#
|
||||
# Repetirlo es la mitigación correcta mientras no haya `patchelf` en el corpus para grabar
|
||||
# `RUNPATH=$ORIGIN` en los ELF, que es lo que hace Alpine y lo que borraría esta variable entera.
|
||||
LD_LIBRARY_PATH="/usr/lib/atuq:/usr/lib/atuq${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export LD_LIBRARY_PATH
|
||||
exec /usr/lib/atuq/atuq "$@"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+143
@@ -0,0 +1,143 @@
|
||||
#!/bin/sh
|
||||
# test-atuq-codecs.sh — ¿qué formatos de vídeo y audio REPRODUCE atuq de verdad?
|
||||
#
|
||||
# scripts/test-atuq-codecs.sh # sale 0 si los seis casos reproducen
|
||||
# scripts/test-atuq-codecs.sh --negative-control # rompe el enlace de ffvpx a propósito
|
||||
#
|
||||
# ── POR QUÉ EXISTE, Y POR QUÉ NO LO CUBRE NINGÚN OTRO GUARDIÁN ────────────────────────────────
|
||||
# `vigia-sonames.py` mira la clausura de un perfil; `test-atuq-rootfs.py` mira los NEEDED del
|
||||
# rootfs hidratado y, con `--dlopen`, adivina el tercer escalón por cadenas. Ninguno de los tres
|
||||
# puede responder la única pregunta que le importa a quien usa el navegador: **¿se ve el vídeo?**
|
||||
# Un códec puede fallar con TODOS los ficheros en su sitio —pasó, y es lo que originó este script:
|
||||
# el proceso RDD perdía `/usr/lib/atuq` del `LD_LIBRARY_PATH`, no cargaba el ffvpx bundleado, se
|
||||
# caía al ffmpeg del sistema, y AV1 —que ese ffmpeg sólo decodifica por hardware— se quedaba en
|
||||
# `readyState=1` para siempre. Sin un error. Sin un NEEDED faltante. Sin una cadena ausente.
|
||||
#
|
||||
# ── CÓMO SE MIDE: EL PROPIO MOTOR DICE EL VEREDICTO ───────────────────────────────────────────
|
||||
# La página abre cada muestra, la deja correr 6 s y mira `currentTime`. Un decodificador que se
|
||||
# crea pero no entrega cuadros deja el tiempo en 0.00, que es exactamente el fallo que buscamos:
|
||||
# «se creó» no es «decodifica». El resultado sale por `dump()` —con
|
||||
# `browser.dom.window.dump.enabled`— al stdout del proceso, así que **no hace falta ni red ni
|
||||
# servidor**: se lee del log. Headless a propósito: esto tiene que correr en el worker y en cron.
|
||||
#
|
||||
# ── LAS MUESTRAS VAN VERSIONADAS, NO SE GENERAN ───────────────────────────────────────────────
|
||||
# `scripts/fixtures/codecs/` son cinco ficheros de ~200 K en total, generados una vez con ffmpeg.
|
||||
# Generarlos en cada corrida ataría el guardián al ffmpeg del HOST —otra versión, otros bytes, otro
|
||||
# resultado— y lo volvería inejecutable donde no haya uno. Un fixture congelado es el control.
|
||||
#
|
||||
# ── EL CONTROL NEGATIVO NO ES ADORNO ──────────────────────────────────────────────────────────
|
||||
# Un guardián que nunca falló y uno que no puede fallar se ven idénticos desde afuera. Con
|
||||
# `--negative-control` se saltea el lanzador y se corre el binario con el appdir UNA sola vez en
|
||||
# `LD_LIBRARY_PATH`, que es la forma exacta del bug: se EXIGE que AV1 falle. Si en ese modo AV1
|
||||
# pasa, el que está roto es este script.
|
||||
set -eu
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
|
||||
RFS="${RFS:-/mnt/cosecha/escritorios/atuq-rootfs}"
|
||||
ALPINE="$ROOT/.dev-fs/alpine"
|
||||
CONTROL=0
|
||||
[ "${1:-}" = "--negative-control" ] && CONTROL=1
|
||||
|
||||
command -v bwrap >/dev/null || { echo "✗ falta bwrap" >&2; exit 1; }
|
||||
|
||||
# El rootfs lo arma `atuq-nested.sh`, que es donde vive LA LISTA DE RAÍCES. Acá no se copia: se
|
||||
# llama. Dos listas es cómo se desincronizan.
|
||||
ROOTFS_ONLY=1 RFS="$RFS" "$ROOT/scripts/atuq-nested.sh"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
cp scripts/fixtures/codecs/* "$TMP/"
|
||||
mkdir -p "$TMP/perfil"
|
||||
cat > "$TMP/perfil/user.js" <<'JS'
|
||||
// `dump()` es el canal de salida del veredicto: sin esto la página no puede hablarle al log.
|
||||
user_pref("browser.dom.window.dump.enabled", true);
|
||||
user_pref("devtools.console.stdout.content", true);
|
||||
// Sin esto un `<video autoplay muted>` puede quedarse bloqueado y el guardián reportaría un
|
||||
// fallo de códec que no existe. Se apaga la política, no el códec.
|
||||
user_pref("media.autoplay.default", 0);
|
||||
user_pref("media.autoplay.blocking_policy", 0);
|
||||
JS
|
||||
|
||||
cat > "$TMP/matriz.html" <<'HTML'
|
||||
<!doctype html><meta charset="utf-8"><title>matriz de códecs</title>
|
||||
<body style="background:#111;color:#eee;font:15px monospace">
|
||||
<h2>¿qué reproduce atuq?</h2><table id="t"></table>
|
||||
<script>
|
||||
const CASOS = [
|
||||
["H264+AAC", "h264-aac.mp4"],
|
||||
["VP9+Opus", "vp9-opus.webm"],
|
||||
["AV1", "av1.mp4"],
|
||||
["MP3", "mp3.mp3"],
|
||||
["FLAC", "flac.flac"],
|
||||
];
|
||||
const t = document.getElementById('t'), res = [];
|
||||
function probar(i){
|
||||
if (i >= CASOS.length) {
|
||||
const s = 'CODEC-RESULT ' + res.join(' ');
|
||||
if (window.dump) dump(s + '\n');
|
||||
console.log(s);
|
||||
return;
|
||||
}
|
||||
const [nom, f] = CASOS[i];
|
||||
const v = document.createElement(/\.(mp3|flac)$/.test(f) ? 'audio' : 'video');
|
||||
v.src = f; v.muted = true; v.autoplay = true; v.playsInline = true;
|
||||
document.body.appendChild(v);
|
||||
let err = '';
|
||||
v.addEventListener('error', () => { err = 'error'; });
|
||||
v.play().catch(() => { err = 'play-rechazado'; });
|
||||
// 6 s: una muestra dura 3 s, así que un decodificador sano llega al final con margen. Y el
|
||||
// veredicto NO es «hubo decodificador» sino «el tiempo avanzó», que es lo que ve el usuario.
|
||||
setTimeout(() => {
|
||||
const bien = v.currentTime > 0.3 && !err;
|
||||
const fila = t.insertRow();
|
||||
fila.insertCell().textContent = nom;
|
||||
fila.insertCell().textContent = bien ? 'REPRODUCE' : 'NO';
|
||||
fila.insertCell().textContent = 't=' + v.currentTime.toFixed(2) + ' ready=' + v.readyState + ' ' + err;
|
||||
res.push(nom + '=' + (bien ? 1 : 0) + '@' + v.currentTime.toFixed(1));
|
||||
v.pause(); v.remove();
|
||||
probar(i + 1);
|
||||
}, 6000);
|
||||
}
|
||||
probar(0);
|
||||
</script>
|
||||
HTML
|
||||
|
||||
# El sandbox de contenido/RDD/utility se apaga DENTRO de bwrap y no por gusto: en una jaula sin
|
||||
# userns anidado cada hijo muere en bucle y no habría decodificación que medir. Es entorno de
|
||||
# prueba, no una preferencia del artefacto — la imagen real corre con su sandbox puesto.
|
||||
if [ "$CONTROL" = "1" ]; then
|
||||
# La forma exacta del bug: sin lanzador y con el appdir UNA vez. Gecko se come el primer
|
||||
# elemento al lanzar el RDD ⇒ ffvpx no carga ⇒ AV1 no reproduce.
|
||||
BIN=/usr/lib/atuq/atuq
|
||||
LDP=/usr/lib/atuq:/usr/lib:/lib
|
||||
else
|
||||
BIN=/usr/bin/atuq
|
||||
LDP=""
|
||||
fi
|
||||
|
||||
bwrap --overlay-src "$ALPINE" --overlay-src "$RFS" --tmp-overlay / \
|
||||
--dev-bind /dev /dev --proc /proc --bind /sys /sys --tmpfs /tmp --tmpfs /run \
|
||||
--bind "$TMP" /salida --uid 0 --gid 0 \
|
||||
--setenv PATH /usr/bin:/bin --setenv LD_LIBRARY_PATH "$LDP" \
|
||||
--setenv HOME /root --setenv XDG_RUNTIME_DIR /run --setenv MOZ_HEADLESS 1 \
|
||||
--setenv MOZ_DISABLE_CONTENT_SANDBOX 1 --setenv MOZ_DISABLE_RDD_SANDBOX 1 \
|
||||
--setenv MOZ_DISABLE_UTILITY_SANDBOX 1 \
|
||||
sh -c "mkdir -p /root; timeout 90 $BIN --no-remote --headless --profile /salida/perfil \
|
||||
file:///salida/matriz.html > /salida/atuq.log 2>&1; true"
|
||||
|
||||
LINEA="$(grep -a '^CODEC-RESULT' "$TMP/atuq.log" | head -1 || true)"
|
||||
[ -n "$LINEA" ] || { echo "✗ el navegador no llegó a dar veredicto — mirá $TMP/atuq.log" >&2
|
||||
cp "$TMP/atuq.log" work/atuq-codecs.log 2>/dev/null || true; exit 1; }
|
||||
|
||||
echo "$LINEA" | tr ' ' '\n' | grep -v CODEC-RESULT | while read -r caso; do
|
||||
printf ' %-14s %s\n' "${caso%%=*}" "$(echo "$caso" | sed 's/.*=//')"
|
||||
done
|
||||
|
||||
MALOS="$(echo "$LINEA" | tr ' ' '\n' | grep '=0@' | sed 's/=.*//' | tr '\n' ' ')"
|
||||
if [ "$CONTROL" = "1" ]; then
|
||||
case " $MALOS " in
|
||||
*" AV1 "*) echo "✓ control negativo: AV1 falla como debe (el guardián SÍ lo vería)"; exit 0 ;;
|
||||
*) echo "✗ control negativo: AV1 pasó sin ffvpx — este script no puede ver el fallo" >&2; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
[ -z "$MALOS" ] || { echo "✗ no reproducen: $MALOS" >&2; exit 1; }
|
||||
echo "✓ los cinco formatos reproducen"
|
||||
Reference in New Issue
Block a user