|
|
# CONTINUE — el media-player (nació del hilo RTL, que ya cerró)
|
|
|
|
|
|
> **Kickoff**: _"Lee `docs/process/CONTINUE-player-rtl.md` y sigue por la cola de §0."_
|
|
|
> **Fecha**: 2026-08-01 · actualizado 2026-08-04 · Rama `alpha-0.1-dir-prefs`
|
|
|
> (sale de `alpha-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:
|
|
|
|
|
|
1. Qué es exactamente «genérico»: ¿patrón fijo, pseudoaleatorio estable derivado
|
|
|
de la fuente, o barras planas?
|
|
|
2. ¿Basta la ausencia de `peaks` como señal de streaming, o hace falta una prop
|
|
|
aparte?
|
|
|
|
|
|
### 0.3 · Defectos vivos, medidos
|
|
|
|
|
|
- **La variante `bar` se estruja a 720px**: el `time-slider` colapsa 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-player` rojo en `eidos/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 o
|
|
|
`KNOWN_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 a `check`. Sobra; no se borró por la regla de
|
|
|
no eliminar sin instrucción explícita.
|
|
|
- Del hilo de dirección: `html lang` no sigue al idioma (el `dir` sí) y la deuda
|
|
|
ajena de `CONTINUE-direction.md` §8.4.
|
|
|
|
|
|
---
|
|
|
|
|
|
> El hilo de audio propiamente dicho sigue en
|
|
|
> [`CONTINUE-sound-engine.md`](./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`](./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 1` en el recipe, no `transform` — la propiedad independiente
|
|
|
COMPONE con lo que un icono escriba inline en vez de reemplazarlo.
|
|
|
- **`scale`, no `rotate: 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 el `dir` del 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.
|
|
|
|
|
|
⚠️ **CORRECCIÓN (2026-08-04, misma jornada)**: la tabla de abajo se leyó mal.
|
|
|
Que la raíz y el pulgar dieran el mismo ±10 **NO significaba que estuvieran de
|
|
|
acuerdo** — significaba que el `seekBy` del player **pisaba** el paso más
|
|
|
pequeño del Slider, porque ambos escriben `currentTime`. Las dos manos actuaban
|
|
|
sobre la misma tecla. Se destapó midiendo con magnitudes distintas (`ArrowUp`
|
|
|
sobre el pulgar del scrubber movía el tiempo **y** el volumen) y está arreglado:
|
|
|
el player exime ahora las teclas que nacen dentro de un `[data-slider]`.
|
|
|
**Lección**: dos manejadores que escriben la MISMA magnitud son
|
|
|
indistinguibles; para detectar doble manejo hay que cruzar ejes.
|
|
|
|
|
|
Medido, un solo `seekStep` por pulsación:
|
|
|
|
|
|
| | 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` / `resume` para hacer algo. Recogiendo el `this` de esas
|
|
|
> llamadas en un `Set` se enumeran las INSTANCIAS, no las creaciones, y un
|
|
|
> contexto preexistente aparece igual. Sin inyección pre-boot y sin tocar
|
|
|
> `app.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 en `globalThis` **en cada llamada, nunca cacheado a nivel de módulo** —
|
|
|
por eso `constructed: 0` significa 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 declara `attach()`**. 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)
|
|
|
|
|
|
- ~~**`Captions`**~~ — **CERRADA 2026-08-04**. Se implementó tal como el
|
|
|
diagnóstico predecía: `mode='hidden'` + pintar `activeCues` en la parte
|
|
|
declarada. El espejo de una sola dirección de `captionsOn` también cerró: el
|
|
|
estado se **deriva** de los modos reales y una pista que el SO encienda se
|
|
|
adopta (degradada a `hidden`, para que no pinte encima). Detalle y las tres
|
|
|
trampas de especificación que impuso —borrado imperativo, siembra del primer
|
|
|
pintado, `activeCues` nulo en `disabled`— 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 que `fetch` de `media.w3.org` y `soundhelix.com` **falla
|
|
|
por CORS** mientras el `<audio>` las reproduce sin problema.
|
|
|
|
|
|
✅ **NUDO RESUELTO — decisión del usuario, 2026-08-04.** Dos piezas:
|
|
|
1. **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 `.json` junto 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.
|
|
|
2. **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 `peaks` ausente basta como señal de streaming o hace
|
|
|
falta una prop aparte.
|
|
|
|
|
|
## 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 `playbackRate`
|
|
|
perfectamente… 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-variant` despué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.
|