21 KiB
CONTINUE — el media-player (nació del hilo RTL, que ya cerró)
Kickoff: "Lee
docs/process/CONTINUE-player-rtl.mdy sigue por la cola de §0." Fecha: 2026-08-01 · actualizado 2026-08-04 · Ramaalpha-0.1-dir-prefs(sale dealpha-0.1-sec-dom). §1, §2, §3 y §4 están CERRADAS y verificadas — no las rehagas. El documento arrancó siendo del eje RTL; hoy es el handoff del componente. Empieza por §0, que es la cola priorizada; §1–§4 son el registro de lo hecho, y se leen sólo si necesitas el porqué de una decisión.
0. La cola — por orden de valor (estado a 2026-08-04)
Dónde está el componente
component:audit --only media-player = PASS (0 errores; los 2 avisos son
falsos positivos del script, que busca MutationObserver y uix.events.emit
como TEXTO en la página y no los ve porque la demo los factoriza en DemoTrace
y SemaPanel — comprobado en el código de ambos).
| Guard | Estado |
|---|---|
check |
0 errores propios (la base global fluctúa por las otras sesiones) |
| vitest soma del player | 14/14 |
eidos-lint media-player |
invalid 0 · class-hooks 0 (morfo-backed 29) |
morfo:check |
player sin fallos (7 preexistentes ajenos) |
smoke de la ruta |
PASS |
perm:check |
SIN CORRER — es el largo; el topbar dio paso a todas las rutas |
Partes declaradas sin implementar: 1 de 23. Con Captions cerrado sólo
queda settings-button.
0.1 · settings-button — el ítem grande restante
Menú de velocidad / calidad / pista. Exige componer DropdownMenu a nivel soma;
la mecánica ya existe (setPlaybackRate, cycleRate, y el RateButton los
usa). Cerrarlo pone el contrato del morfo en 23/23, que es lo único que
separa al componente de tenerlo completo.
0.2 · Waveform como scrubber — DESBLOQUEADO, faltan dos precisiones
Las decisiones del usuario (§5) resolvieron el nudo. Antes de implementar hay que decidir —y NO improvisar— dos cosas:
- Qué es exactamente «genérico»: ¿patrón fijo, pseudoaleatorio estable derivado de la fuente, o barras planas?
- ¿Basta la ausencia de
peakscomo señal de streaming, o hace falta una prop aparte?
0.3 · Defectos vivos, medidos
- La variante
barse estruja a 720px: eltime-slidercolapsa a ancho 0 y el mute a 19px (los demás miden 36). Pasa igual en LTR y RTL, así que no es del eje de dirección — la fila no tiene sitio para lo que mete. audio-playerrojo eneidos/lint.test.ts: la capa de variantes no tiene morfo propio y el guard no tiene categoría para «capa de variantes sobre morfo hermano». Es un guard compartido; decisión pendiente (¿categoría nueva oKNOWN_MISSING_MORFO?). No tocar en solitario.
0.4 · Sin verificar por ojos
La matriz 4 variantes × claro/oscuro × RTL. Cubierto a 2026-08-04: vídeo
LTR/RTL, audio card en RTL, y los subtítulos en claro y oscuro. Faltan
row / bar / inline y casi todo el modo oscuro.
0.5 · Diferidos con disposición escrita (no son deuda oculta)
Doble-tap-seek · preview de miniatura en el scrubber —que es un gap del
Slider, no del player— · migrar los botones a renderProps, que es un
barrido global del catálogo.
0.6 · Ajeno al player, abierto por estas sesiones
- El interruptor Sound del topbar no silencia los earcons (§4.2). Es de la app de demos, no del framework.
src/arts/adom/__scratch-verify.ts: fichero de sonda que dejó un agente, sin trackear, suma 1 error acheck. Sobra; no se borró por la regla de no eliminar sin instrucción explícita.- Del hilo de dirección:
html langno sigue al idioma (eldirsí) y la deuda ajena deCONTINUE-direction.md§8.4.
El hilo de audio propiamente dicho sigue en
CONTINUE-sound-engine.md; este documento recoge lo que la mirada del usuario abrió el 2026-08-01 y quedó SIN cerrar.
Lo que se cerró el 2026-08-01 (7 commits, verificados)
| Commit | Qué |
|---|---|
df86a4e50 |
El anillo del commit dejaba de enmarcar contenedores (slider + media-player) · saltar al final ya no anuncia compleción falsa |
e096f65e3 |
MediaSession verificada por el usuario (carátula + título + artista en el hub de Chrome) |
905012507 |
Waveform: el buffered vuelve a banda recta con acento lavado |
58acb778e |
Waveform F6 firmada — iniciativa COMPLETA |
77f8a4e46 |
La demo del waveform carga solo la pista real; sample deja de fingir que es una prop |
4356020e3 |
La carátula de card sale a token (--audio-player-artwork-size-card) |
3ceda5b59 |
El botón de velocidad ya dice su velocidad (emit: 'value') · el player reenvía dir a sus dos sliders |
1. PRIORIDAD — el Slider está roto en RTL · CERRADO 2026-08-02
Cerrado en 013ceac57 + 5469e05df (rama alpha-0.1-dir-prefs). La
hipótesis de este documento acertó las dos piezas visibles — el Slider sí
estampaba dir y slider.css sí mezclaba lógicas con físicas — pero eran
síntomas, no la causa. Lo que resultó ser, medido:
| Del slider | slider.css emparejaba inset-inline-start: 50% (lógico) con translateX(-50%) (FÍSICO, no se voltea) en las tres reglas verticales. Medido en Chrome: raíz/pulgar/ticks en x=961, raíl y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ése era el idioma correcto del propio fichero. Mismo defecto en accordion.css (text-align: left), el único físico que quedaba en todo eidos. |
| La causa raíz, que no estaba en las hipótesis | makeDimension().get() (arts/prefs) leía engine.snapshot() esquivando la celda $state, así que soma.prefs.getDir() era una lectura sin tracking y los ~64 componentes que resuelven su dirección desde prefs la congelaban al montarse. La proyección DOM se salvaba porque usa onChange: misma dimensión, dos caminos de lectura, uno solo reactivo. |
| Y el estampado | 33 de 37 componentes escribían dir con un valor SIEMPRE concreto → una app que ponga <html dir="rtl"> sin registrar la preferencia se encontraba 33 islas del revés. Ahora el atributo se omite cuando nadie afirmó nada (modelo de Zag, aplicado uniforme). |
No hizo falta elegir «un solo dueño de la inversión»: el JS sigue calculando físico y el CSS sigue siendo lógico. No había doble volteo — había un transform físico donde tocaba uno lógico.
✅ La contradicción del pulgar del volumen queda explicada: con getDir()
congelado, el mismo slider daba lecturas distintas según cuándo se midiera
respecto al montaje. No era una sonda mintiendo.
Guards que nacieron en rojo y ahora pasan:
arts/prefs/test/active-prefs-reactivity.svelte.test.ts (intent · derivación ·
granularidad por clave) y uix/active-uix/test/prefs-view.svelte.test.ts.
De regalo: elegir árabe voltea la dirección por derivación desde el idioma —
la cadena que active-architecture.md:407 documenta y que en esta app era
código muerto, porque la shell fijaba intent desde el primer frame y no había
ningún idioma RTL en el catálogo.
⚠️ Sigue abierto de este eje: html lang no sigue al idioma (el dir sí), y
perm:check sin ejecutar — el topbar ya lleva data-perm-step, lo que hace que
toda ruta tenga paso y la pasada sea mucho más larga.
2 y 3. Disposición de los botones + iconos de seek · CERRADAS 2026-08-04
Eran el mismo defecto visto desde dos lados. Las sondas de aquel día
acertaban y §2 no era un defecto de disposición: re-medido con el contexto
verificado ANTES de cada número (data-media / data-variant / data-dir
leídos tras cada clic), la geometría mirroriza exacta en las cinco
disposiciones — vídeo con DefaultControls y audio en card · row · bar ·
inline. Cada x en RTL es el espejo del LTR ±1px, comprobado punto por punto.
Lo que el ojo rechazaba eran los glifos, que no se volteaban
(transform: none, rotate: none). En RTL el transporte se leía ▶▶ ▶ ◀◀:
las dos flechas exteriores apuntando HACIA DENTRO. Es literalmente el
síntoma que carousel.css:235 y §9.9 de CONTINUE-direction.md
ya habían documentado y arreglado en los chevrons del carousel.
| LTR (izq→der) | RTL antes | RTL ahora | |
|---|---|---|---|
| transporte (vídeo) | ▷ ◁◁ ▷▷ |
▷▷ ◁◁ ▷ ✗ dentro |
◁◁ ▷▷ ◁ ✓ fuera |
| transporte (audio) | ◁◁ ▷ ▷▷ |
▷▷ ▷ ◁◁ ✗ dentro |
◁◁ ◁ ▷▷ ✓ fuera |
Alcance elegido por el usuario: todo lo direccional — seek · play/pause ·
altavoz de volumen/mute · esquina del PiP. Fuera se quedan fullscreen (sus
cuatro esquinas son simétricas) y captions, que es la única llamada que el
catálogo no ha hecho: si el glifo de subtítulos cuenta como direccional está
sin decidir, y se dejó SIN voltear.
El mecanismo, heredado del precedente del carousel:
scale: -1 1en el recipe, notransform— la propiedad independiente COMPONE con lo que un icono escriba inline en vez de reemplazarlo.scale, norotate: 180deg: sólo coinciden en glifos verticalmente simétricos. Rotar el icono del PiP le llevaría el recuadro a la esquina superior izquierda en vez de a la inferior izquierda.- Anclado a
[data-media-player][data-dir='rtl']— el atributo PROPIO del componente, no eldirdel DOM, así que no es el[dir='rtl']prohibido y garantiza que el glifo coincida con la matemática del teclado, que resuelve de esa misma fuente.eidos-lint media-player: invalid 0 · class-hooks 0.
De paso: el --slider-range-bg del scrubber era un linear-gradient(90deg)
FÍSICO, así que en RTL la rampa corría al revés respecto al sentido en el que
crece el rango. Par :dir() con 270deg, como la rampa de saturación del
color-picker.
2.1 · El dir del player estaba PARTIDO POR LA MITAD — encontrado por el camino
<MediaPlayer dir="rtl"> sin dir ambiental dejaba el cromo dispuesto en
LTR y sus dos Sliders en RTL. El wrapper pasaba la prop CRUDA
(readableActive(() => dir)) y el provider sólo la reenviaba a los sliders: no
resolvía la cadena ni estampaba nada. En la demo estaba enmascarado porque
el stage estampa dir además de pasar la prop — por eso ninguna medida anterior
lo vio.
⚠️ El censo de canonización de CONTINUE-direction.md §9.15 no lo tenía: el
media-player no es ni canónico, ni grupo A, ni grupo B. Era un cuarto caso —
expone dir, pero no lo resuelve NI lo estampa. Si alguien vuelve a auditar la
canonización, ese censo cuenta wrappers y providers, no componentes que
simplemente reenvían la prop a un hijo.
Canonizado con el patrón de §9.16: activeDir(() => dir, soma) en el wrapper ·
resolvedDir en el provider · dir crudo + data-dir resuelto en los props.
El tipo pasó de dir?: Direction a dir: Direction | undefined (la clave
siempre la pone el wrapper; sólo el VALOR puede faltar) — ensanchar el tipo
primero, que es lo que destapó los 11 puntos del test.
2.2 · El teclado seguía siendo físico
El morfo declaraba ArrowLeft→seek-backward / ArrowRight→seek-forward, ciego
a la dirección, mientras soma ya tiene getDirectionalKeys(dir, orientation)
justo para esto y el Slider embebido sí es direccional. Decisión del usuario:
direccional. Ahora resuelve por ese helper. j / l se quedan quietas: son
alias semánticos, no flechas.
Medido, y raíz y pulgar de acuerdo (un solo seekStep, sin doble salto):
| LTR | RTL | |
|---|---|---|
ArrowRight (raíz / pulgar del scrubber) |
+10 | −10 |
ArrowLeft (raíz / pulgar del scrubber) |
−10 | +10 |
2.3 · Verificación
check 74 = línea base · rtl:check 1 (el de palabras, preexistente) ·
morfo:check 7 fallos, ninguno del player (preexistentes) ·
eidos-lint media-player invalid 0 · vitest del player + los dos guards de
dirección 16/16. A ojo en el Chrome real del usuario, A/B completo en LTR y
RTL, vídeo y audio/card.
4. Medición del escenario mixto · CERRADA 2026-08-04
La receta que este documento prescribía no hacía falta. «Instrumentar antes del boot + navegación client-side» era la respuesta a la pregunta equivocada. El problema real es que se parcheaba el CONSTRUCTOR, que sólo puede responder «¿se creó uno nuevo?». La solución es parchear el PROTOTIPO:
Cualquier contexto —nacido cuando sea— tiene que llamar a
createGain/createOscillator/resumepara hacer algo. Recogiendo elthisde esas llamadas en unSetse enumeran las INSTANCIAS, no las creaciones, y un contexto preexistente aparece igual. Sin inyección pre-boot y sin tocarapp.html.
Con eso, medido en el Chrome del usuario sobre /uix/components/media-player,
audio card, pista real de 6:12 sonando sin mutear a volumen 1:
| Contextos distintos que trabajaron | 1 (running, 44,1 kHz) |
| Earcons disparados encima | 26 (52 osciladores + 78 gains + 26 biquads — el par de osciladores + biquad lowpass que describe PLAN-sound-engine.md §4) |
createMediaElementSource |
0 — la media NUNCA entra en el grafo |
| Contextos cerrados / churn | 0 / 0 (contraste con el «antes» de §15.2: 10 creados, 9 cerrados) |
La invariante de un solo AudioContext por documento se sostiene en el
escenario mixto.
4.1 · Y el contexto preexistente, por fin fechado
Es real y la instrumentación vieja no podía verlo. Con la sonda instalada en
t=10,9s y el primer earcon a t≈30s, el reloj del contexto marcaba 25,86s → el
contexto nació en t ≈ 4,1s, sin ningún gesto de usuario, y ya running. Dos
señales independientes lo confirman: constructed: 0 (el parche del constructor
no lo vio nacer) y la primera aparición es createOscillator, no
createGain — los tres createGain que createContext() hace de inmediato
para el master y los dos buses ya habían pasado.
⚠️ Mi primera hipótesis —que lo creaba el registro de sound.media(el) del
player— es FALSA, y el control la tumbó: en /uix/components/button, una
página sin ningún elemento media, el contexto nace en t ≈ 4,08s, lo mismo.
Es del arranque de la app, no del reproductor.
Auditoría de código en paralelo (29 agentes, cada afirmación refutada adversarialmente) que respalda la lectura del navegador:
- Sitio de construcción único:
arts/sound/engine-sound.ts:452, y el ctor se busca englobalThisen cada llamada, nunca cacheado a nivel de módulo — por esoconstructed: 0significa de verdad «nació antes del parche» y no «se coló por una referencia capturada». - Embudo único
createContext()con tres llamadores (prime·getOrCreateContext·ensureContext); el motor no toca Web Audio hasta uno de ellos. - El player estructuralmente no puede meter la media en el grafo: guarda el
handle tipado como
MediaProvider, puerto que no declaraattach(). Es más fuerte que «es opt-in y nadie lo activa».
4.2 · Hallazgo NUEVO y ajeno: el interruptor de Sound del topbar no silencia
Salió de la auditoría y lo verifiqué midiendo. Con data-sound="reduce"
(interruptor OFF, soundOn: false persistido) un clic en un Button sigue
construyendo 2 osciladores + 3 gains, y la envolvente es idéntica:
Sound ON [0, 0.25, 0.25, 0.0001]
Sound OFF [0, 0.25, 0.25, 0.0001] ← mismo pico, mismos nodos
El toggle escribe el intent en prefs y estampa data-sound en <html>, pero el
root de composición (web/routes/uix/+layout@.svelte) no pasa preferences
al canal de sonido de sema, que cae a nivel 'full'. El framework sí lo
soporta (sema/engine.ts reenvía soundOptions.preferences ?? opts.preferences);
es la app de demos la que no lo cablea.
⚠️ Alcance de lo que medí: la envolvente del PROPIO earcon. No medí una ganancia de bus aguas abajo, así que no puedo afirmar «se oye igual de fuerte» — eso lo zanja el oído. Dos líneas independientes (medida + auditoría) apuntan a que el interruptor es inerte. Es de la app de demos, no del player, y no se tocó.
4.bis · Ajeno al eje, encontrado midiendo — la variante bar se estruja
A 720px de ancho y size='md', la variante bar deja el time-slider en
ancho 0 y el botón de mute en 19px (los otros miden 36). Pasa igual en
LTR y en RTL, así que no es del eje de dirección: la fila no tiene sitio para
todo lo que mete. Sin tocar.
5. Iniciativas dormidas (cada una con su gate)
-
— CERRADA 2026-08-04. Se implementó tal como el diagnóstico predecía:Captionsmode='hidden'+ pintaractiveCuesen la parte declarada. El espejo de una sola dirección decaptionsOntambién cerró: el estado se deriva de los modos reales y una pista que el SO encienda se adopta (degradada ahidden, para que no pinte encima). Detalle y las tres trampas de especificación que impuso —borrado imperativo, siembra del primer pintado,activeCuesnulo endisabled— en el README de eidos, §Captions. Decisión declarada: el marcado inline se conserva, la metadata de posición se descarta. -
Waveform como scrubber del player — cero cables hoy entre ambos, y es deliberado (D-AP2.4 +
PLAN-waveform.md§0 lo aplazan a la v2 del player). El nudo NO era cambiar el componente: era de dónde salen los picos. El contrato del waveform (D-WF.2) dice que los provee la app, y un player streamea, así que no puede decodificar su fuente sin bajársela entera. Verificado además quefetchdemedia.w3.orgysoundhelix.comfalla por CORS mientras el<audio>las reproduce sin problema.✅ NUDO RESUELTO — decisión del usuario, 2026-08-04. Dos piezas:
- Los picos se pasan como PROP. No se generan en el player ni se
regeneran solos: los provee quien los tenga (el backend que ya los calculó
al subir el fichero, un
.jsonjunto al audio, una decodificación previa). Ratifica D-WF.2 y lo lleva a la API del player. Evita, además, el coste de recalcularlos en cada montaje. - Si la fuente se streamea, waveform GENÉRICO. Sin picos que decodificar, el player pinta una onda genérica y sigue sirviendo de scrubber. Se acabó el bloqueo del CORS y el «bajarse la pista entera»: el caso sin datos tiene ahora una respuesta declarada en vez de quedarse sin componente.
Queda por decidir al implementarlo (NO decidido): qué es exactamente «genérico» — patrón fijo, pseudoaleatorio estable derivado de la fuente, o barras planas — y si
peaksausente basta como señal de streaming o hace falta una prop aparte. - Los picos se pasan como PROP. No se generan en el player ni se
regeneran solos: los provee quien los tenga (el backend que ya los calculó
al subir el fichero, un
Lecciones de método del 2026-08-01 — leer antes de repetirlas
- Medir el contrato NO es verificar la experiencia. Se afirmó tres veces
que algo estaba verificado midiendo JS: la MediaSession (metadata poblada
≠ overlay del SO visible), el botón de velocidad (cambiaba
playbackRateperfectamente… y no lo mostraba) y el RTL (se comprobó LTR y se dio RTL por bueno). Las tres las cazó el usuario. - El panel del navegador embebido NO SIRVE para nada visual:
innerWidth: 0,visibility: hidden, el stage mide 2px. Ese día inventó tres «overflow» inexistentes y un hueco de carátula que estaba bien. Screenshot falla con «not compositing frames». Para cualquier juicio visual, usar el Chrome real del usuario (mcp__claude-in-chrome__*). - Comprobar que el control se pulsó de verdad. Dos medidas de esa sesión
midieron el modo vídeo creyendo que era audio porque el chip no cambió y
nadie lo verificó. Leer siempre
data-media/data-variantdespués de clicar. - La distancia RGB engaña: avaló un gris que el ojo del usuario rechazó al instante. Está dominada por la luminosidad y es ciega al croma.
- La forma manda sobre el color: el buffered del waveform se rechazó dos veces; la segunda porque una copia de la onda es la onda y se camufla por construcción, por muy distinto que sea su color.