Files
takana/recipes/atuq/extensions/medios/fondo.js
T
Sergio 928d5eb119 atuq: un vídeo se abre en el reproductor de la distro, no en una pestaña
El 6.6 de la unidad 9 del SDD 26. Una navegación de PRIMER NIVEL a un medio
(`Content-Type: video/*` o `audio/*`) se cancela y la URL se la lleva `mpv`, que ya viaja en las
cuatro imágenes de escritorio: esto no agrega ni una receta, es cablear lo que ya estaba.

    MEDIO http://…/audio.wav
    CANCELADA la navegación http://…/audio.wav
    ABIERTO /usr/bin/mpv pid=266 http://…/audio.wav
    mpv: AO: [null] 8000Hz mono 1ch u8        ← su propio log: DECODIFICÓ, no sólo arrancó

LA REGLA ES ESTRECHA A PROPÓSITO: sólo el primer nivel. Un `<video>` embebido es parte de la página y
sacarlo de ahí rompería el sitio que lo puso. Las reglas anchas en el camino de cada petición son las
que terminan rompiendo la web de alguien.

FAIL-OPEN, y es lo que prueba el control negativo: cancelar es SÍNCRONO y la respuesta del host llega
después, así que no se puede saber en el momento si el reproductor arrancó. Se cancela sólo con el
puerto vivo y, si el host contesta que no pudo, la URL VUELVE al navegador con una marca para no
entrar en bucle:

    SIN REPRODUCTOR no hay reproductor en /usr/bin/mpv: esta imagen no lo trae
    VUELVE AL NAVEGADOR http://…/audio.wav?atuq-medios=no

Quedarse sin vídeo Y sin pestaña es el único resultado inaceptable, y es justo el que se consigue si
uno confía en que salió bien.

⚠ EL BUG QUE ESTE GUARDIÁN DESTAPÓ VALE MÁS QUE LA FUNCIÓN, y estaba DESPUÉS del éxito aparente: la
primera corrida detectó el medio, canceló y recibió el pid… y después el puerto murió con «Native
application tried to send a message of 546281442 bytes». **Un hijo hereda los descriptores del padre,
y los del padre SON la tubería de native messaging**: `mpv` escribía su salida ahí y el navegador la
leía como un marco. Arreglado en tawasuyu (`2e99d216`, pineado acá) con tres redirecciones, cada una
contra un fallo distinto — `stdin` a null porque mpv LEE stdin y se comería los mensajes del
navegador; `stdout` al stderr del host y no a `/dev/null`, porque apagarlo arreglaría el bug y se
llevaría puesto el diagnóstico; `stderr` heredado por lo mismo.

Y dos cosas del ARNÉS, las dos ya vistas antes en esta misma sesión:
· la URL NO se pasa por la línea de comandos: una petición del arranque compite con la
  inicialización de la extensión (la misma carrera del guardián de `sct`). El guardián navega a una
  página que redirige a los 3 s, que además es lo que hace una persona: seguir un enlace;
· la evidencia de que el reproductor CORRIÓ se le pide a su propio `log-file`, porque su salida ya no
  va al stdout del host y Gecko no vuelca el stderr del host al del navegador. Sin eso sólo se sabría
  que hubo un `spawn`.

Del §6 quedan el foco (6.5), la IA local (6.7) —que además es la que traería el motor que le falta al
archivo del §6.3— y el torrent (6.9). Medido de paso: **el corpus no tiene ninguna receta de LLM ni de
embeddings**, así que 6.7 hoy no se puede pagar.

Medido sobre `atuq b3:a7080058` y `puriy-costura b3:060314e6`.
2026-09-10 15:45:08 +00:00

122 lines
4.5 KiB
JavaScript

"use strict";
// medios — el vídeo no se ve en una pestaña (SDD 26 §6.6), v1.
//
// Cuando una navegación de PRIMER NIVEL resulta ser un medio —el servidor contesta
// `Content-Type: video/*` o `audio/*`—, la navegación se cancela y la URL se le pasa
// al host, que abre el reproductor de la distro. El navegador vuelve a ser un
// navegador: no un reproductor a medias metido en una pestaña.
//
// ── POR QUÉ SÓLO EL PRIMER NIVEL, Y NO LOS `<video>` DE UNA PÁGINA ────────────
// Un `<video>` embebido es parte de la página: sacarlo de ahí rompería el sitio que
// lo puso. Lo que esta extensión cambia es el caso en que el navegador NO estaba
// pintando una página, sino haciendo de reproductor: seguiste un enlace a un `.mp4`.
// Es una regla estrecha a propósito — las reglas anchas en el camino de cada
// petición son las que terminan rompiendo la web de alguien.
//
// ── LA REGLA DE ORO: SI NO HAY REPRODUCTOR, NO SE CANCELA NADA ────────────────
// La decisión de cancelar es SÍNCRONA y la respuesta del host llega después, así que
// no se puede saber en el momento si el reproductor arrancó. Se cancela de forma
// optimista **sólo si el puerto está vivo**, y si el host contesta que no pudo, la
// URL se vuelve a abrir en el navegador. Quedarse sin vídeo y sin pestaña sería el
// peor resultado posible, y es el que se consigue si uno confía en que salió bien.
const HOST = "puriy_costura";
// Los tipos que se sacan del navegador. Deliberadamente cortos: `application/*` no
// entra —hay demasiadas cosas que se sirven así— y los manifiestos de streaming
// tampoco por ahora, porque el reproductor los pide con otras cabeceras.
const MEDIOS = ["video/", "audio/"];
let puerto = null;
// url → tabId, para poder devolver al navegador lo que el host no pudo abrir.
const enVuelo = new Map();
function di(s) {
try { dump("MEDIOS " + s + "\n"); } catch (e) {}
}
function conectar() {
try {
puerto = browser.runtime.connectNative(HOST);
puerto.onMessage.addListener(alRecibir);
puerto.onDisconnect.addListener(p => {
di("DESCONECTADO " + (p.error ? p.error.message : "sin error"));
puerto = null;
});
di("CONECTADO " + HOST);
} catch (e) {
di("SIN HOST " + e.message);
puerto = null;
}
}
function alRecibir(r) {
if (!r || r.verb !== "media.open") {
if (r && r.ok === false) { fracaso(r); }
return;
}
if (r.ok === true && r.player) {
di("ABIERTO " + r.player.program + " pid=" + r.player.pid + " " + r.player.url);
enVuelo.delete(r.player.url);
return;
}
fracaso(r);
}
// El host no pudo. La navegación ya está cancelada, así que hay que devolverla: sin
// esto el usuario se queda sin vídeo Y sin pestaña.
function fracaso(r) {
di("SIN REPRODUCTOR " + (r && r.error ? r.error : "sin error"));
for (const [url, tabId] of enVuelo) {
enVuelo.delete(url);
try {
// `?atuq-medios=no` marca la reapertura para no volver a cancelarla: sin esa
// marca esto sería un bucle —cancelar, reabrir, cancelar— con la pestaña
// parpadeando y sin vídeo.
const vuelta = url + (url.includes("?") ? "&" : "?") + "atuq-medios=no";
browser.tabs.update(tabId, { url: vuelta });
di("VUELVE AL NAVEGADOR " + vuelta);
} catch (e) {
di("NO PUDE DEVOLVERLA " + e.message);
}
}
}
function esMedio(cabeceras) {
for (const h of cabeceras || []) {
if (h.name.toLowerCase() === "content-type") {
const v = (h.value || "").toLowerCase();
return MEDIOS.some(m => v.startsWith(m));
}
}
return false;
}
browser.webRequest.onHeadersReceived.addListener(
d => {
try {
if (d.url.includes("atuq-medios=no")) { return {}; } // ya volvió una vez
if (!esMedio(d.responseHeaders)) { return {}; }
if (!puerto) { conectar(); }
if (!puerto) {
// Sin host no se cancela: mejor un vídeo en una pestaña que ningún vídeo.
di("SIN HOST, no se cancela " + d.url);
return {};
}
di("MEDIO " + d.url);
enVuelo.set(d.url, d.tabId);
puerto.postMessage({ verb: "media.open", url: d.url });
di("CANCELADA la navegación " + d.url);
return { cancel: true };
} catch (e) {
di("FALLÓ el filtro " + e.message);
return {};
}
},
{ urls: ["http://*/*", "https://*/*"], types: ["main_frame"] },
["blocking", "responseHeaders"]
);
conectar();
di("LISTA v1 saca del navegador sólo la navegación de primer nivel");