You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/PLAN-audio-player-v2.md

117 lines
17 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# PLAN v2 — Reproductor de sonido, SOBRE el servicio `$sound`
> **Tipo**: plan de creación por fases (process — efímero, NO fuente de verdad).
> **Fecha**: 2026-07-31 · **Estado**: **✅ GATE FIRMADO 2026-07-31** («ok, continua»
> tras la presentación única, tras dos preguntas de diligencia sobre el estado y
> el API del engine): **las 13 con las recomendaciones de la tabla** — incluidas
> las sub-decisiones **H-9 = CABLEARLO** (D-AP2.10) y **D-AP2.7 = solo
> `duckUiWhileContent` en v1**. F1 en curso.
> Sustituye a [`PLAN-audio-player.md`](./PLAN-audio-player.md) (gate v1 rechazado
> de raíz el 2026-07-31 por el SUSTRATO de sonido, hoy rediseñado y auditado:
> [`PLAN-sound-redesign.md`](./PLAN-sound-redesign.md)). **Los HECHOS del v1
> siguen válidos y NO se re-excavan**: la matriz comparativa (§2 v1, verificada
> 2026-07-30, segmentos = 1 de 6), los hallazgos H-1…H-10, las verificaciones
> de F0 (§8.5–§8.7 v1), el contrato propuesto (§7 v1) y las formas (§6 v1).
> Este plan re-presenta las decisiones ancladas al servicio; el razonamiento
> largo vive en el v1 y aquí se referencia, no se copia.
> **Kickoff para sesión nueva**: *"Lee `docs/process/PLAN-audio-player-v2.md`;
> si el gate está firmado, continúa por la fase abierta."*
>
> **Precondición de lecturas (hechas 2026-07-31)**: provider soma completo ·
> morfo `media-player` · morfo `slider` · `DefaultControls` · puerto
> `MediaProvider`. Quedan para ABRIR F1 (donde muerden): `slider-provider`
> completo + recipe del scrubber (`feedback_study_real_recipes_before_building`).
---
## 0. Qué cambió el sustrato — decisiones v1 que se DISUELVEN
| v1 | Qué pasó |
| --- | --- |
| D-AP.5 (MediaSession, riesgo singleton) | **Del servicio** (D-SR.6 firmada): el player pasa `metadata` y no toca `navigator.mediaSession`. El riesgo de dos players pisándose lo arbitra el servicio (fuente activa + promoción) |
| D-AP.11 (¿el player consume `$sound`?) | **Disuelta** (D-SR.4 firmada): el transporte ES el handle del servicio (`uix.sound.media(el)`, forma exacta del puerto `MediaProvider` — drift-guard ya en verde). `attach()` existe y NO se usa en v1 |
| D-AP.7 (silencio sema en audio, ¿quién restaura el master?) | El dueño existe: **`duckUiWhileContent`** (bus `ui` con retención refcount). Queda UNA decisión de alcance → D-AP2.7 |
| §7.6 invariantes (prefs vs volumen de la obra; silencio alcanza descendientes) | **Estructurales**: el volumen de la obra es de la fuente/`content`; el duck del bus `ui` cubre TODOS los earcons (descendientes incluidos) sin reglas de cascada |
## 1. La migración (el trabajo nuevo del v2)
1. **`registerMedia` cambia su default**: `createProvider ?? (el => this.soma.uix?.sound ? this.soma.uix.sound.media(el, { metadata }) : nativeMediaProvider(el))` — el port `createProvider` SE CONSERVA (hls.js/dash siguen enchufables); sin `uix` degrada al adaptador local.
2. **El adaptador nativo de soma SE RETIRA** al completar la migración (sin shim, doctrina de la casa) y con él el drift-guard transicional (`media-provider-drift.test.ts`) — su trabajo habrá terminado. Un adaptador custom sobre elemento delega en `uix.sound.media(el)`; un embed sin elemento (YouTube) queda **sin ciudadanía** (limitación declarada en README).
3. **Prop nueva `metadata`** (`SoundMediaMetadata`) en el Provider — la identidad que Artwork/Title/Artist pintan es la MISMA que se proyecta al SO.
4. **G-5 aquí es cirugía concreta** (leído el provider): mover `trigger('commit-toggle-play')` de `togglePlay()` al evento `play`/`pause` de `applySnapshot` (el patrón que `commit-complete` ya usa con `ended`); ídem `toggleMute` → `volumechange`.
5. Los **pendientes del hilo del servicio** aterrizan aquí: medición Chrome del escenario mixto (earcon + media, 1 contexto) y prueba de teclas de medios — F7.
## 2. Decisiones — gate D-AP2 (SIN FIRMAR)
| ID | Cuestión | Recomendación |
| --- | --- | --- |
| **D-AP2.1** | Pertenencia | = v1 D-AP.1(a): un motor, un contrato — `media-player` gana partes audio-only `optional` + layout eidos (razonamiento v1 §4) |
| **D-AP2.2** | Puerta pública | = v1 D-AP.2(c): `<MediaPlayer.AudioLayout/>` (espejo de `DefaultControls`, leído) + raíz competidora `<AudioPlayer>` (precedente Toast/Toaster) |
| **D-AP2.3** | Formas | = v1 D-AP.3(c): `variant: card·row·bar·inline` + `size` canónico, ambos responsive (detalle §6 v1) |
| **D-AP2.4** | Waveform | = v1 D-AP.4(a): componente canónico, iniciativa hermana POST-v1 (`peaks` de la app; `$libs/plots`) |
| **D-AP2.5** | Identidad → SO | **Nueva forma de D-AP.5**: prop `metadata` reenviada a `sound.media(el, { metadata })`; opt-in como siempre; el player NUNCA toca MediaSession |
| **D-AP2.6** | Cola / playlist | = v1 D-AP.6(b): fuera (blocks/app; `next-features.md`) |
| **D-AP2.7 v2** — REVISADA Y FIRMADA 2026-07-31 («lo firmo») | Semántica de los botones del player | **El transporte del player es SILENCIOSO POR DEFECTO, por definición** (D.7: su output ES audio; un tick de UI compite con la obra). Pack con el patrón D.5 (resta exacta del gain de familia — las primitivas del intent intactas, `threat`/`fulfill` afloran): `commit-toggle-play`/`-mute`/`commit-set-rate` en sus botones + **reglas de descendientes** (H-10) para el `contact-activate` de los Buttons compuestos (tuning nuevo `contact.silent`, −0.25) y el `commit-set` de los Sliders compuestos (scrubber y volumen). **Intactos**: `commit-fail` (risk debe oírse) y `commit-complete` (informa de la obra; revisitar con escucha real). **AMPLIADA por escucha real (mismo día, «los botones ya no… pero los sliders sí»)**: `handle-pick`/`handle-drag` de los Sliders compuestos → **silencio por CANAL** (`channels: ['haptic']`, reglas de descendientes de mayor especificidad) — ambos son ESTRUCTURALES sin intent (el libro concentra la evaluación en el drop, ya cubierto por la resta de `commit-set`), así que tirar su canal sonoro no borra carga evaluativa; el resolver del drag posee las primitivas del sonido, pero una señal con el canal inactivo no suena. Mi nota anterior («no callable desde el pack») era conservadora de más — corregida. Reactivación de la voz = capa de app (cascada 5b), sin prop nueva. Complementario con `duckUiWhileContent` (el pack calla AL PLAYER; el duck atenúa al RESTO de la app). Háptica se queda (no compite con la obra). Verificado: selectores 100% del builder tipado (solo el combinador es literal) · vocabulario limpio · sema 192/192 · check 0 propios |
| **D-AP2.8** | Capítulos + transcripción | = v1 (b): v2 con disposición; capítulos dependen de G-2 |
| **D-AP2.9** | Responsive | = v1 (a): `ResponsiveProp` en v1; container queries evaluadas en F6 |
| **D-AP2.10** | Alcance v1 | Paridad (§2 v1) + superaciones vía servicio (metadata→SO · duck) · **sub-decisión H-9**: `sustain-loading` (evento declarado JAMÁS emitido) — **cablearlo** (2 líneas: `waiting`→emit, clear en `canplay`; recomendación, el spinner ya existe y el morfo lo promete) o retirarlo del morfo |
| **D-AP2.11** | Providers custom | **Nueva**: el port `createProvider` se conserva; adaptadores sobre elemento delegan en el servicio; embeds sin elemento = sin ciudadanía (declarado). El adaptador nativo local se retira |
| **D-AP2.12** | Segmentos (`clip`) | = v1 D-AP.12(a): v2, diseño CONGELADO en v1 §7.5; (b) sigue en pie solo si hay caso de uso hoy |
| **D-AP2.13** | Scrubber duplica `commit-set` | = v1 D-AP.13(b): retirar `commit-set-time` del morfo y delegar en el Slider embebido (el provider ya practica el patrón en `setVolume`) |
## 3. Fases (tras la firma)
| Fase | Contenido | Verify |
| --- | --- | --- |
| **F0 — Firma** | Presentar este gate UNA vez | Tabla firmada en el chat; plan actualizado |
| **F1 — Gaps de framework** ✅ **COMPLETA 2026-07-31 (G-4 · G-3 · G-1 · G-2)** — G-2: parte `SecondaryRange` en el morfo + `SliderSecondaryRangeProvider` (espejo de Range: inline `left/width%` desde el inicio, RTL/vertical; 0% sin valor) + prop `secondaryValue?` + `Slider.SecondaryRange` en soma/eidos + regla CSS bajo la de range + token `--slider-secondary-bg` (28% del primario, entre pista y range) regenerado. ⚠️ Lección: claves nuevas de `ActiveProps` OPCIONALES (`?`) — requeridas rompieron a los 7 consumidores de `SliderProvider.create` (color/time/time-range-picker), corregido. Verificado: slider 11/11 · `component:audit --only slider` **PASS** · eidos-lint morfo-backed · `check` 74 con **0 propios** (incl. consumidores) | Lecturas hechas: `slider-provider` completo · regla `[data-slider-range]` de la recipe · wrappers soma/eidos · **G-4 ✅** selector del pack a `play-button` (declarado: play/pause pasa a sonar con el tap sutil del pack, antes regla muerta → family default; `morfo:vocabulary` limpio, sema 192/192) · **G-3 ✅** `formatDuration` en `$libs/days/format.ts` + tests (19/19; el consumo del provider llega en F3; dígitos no localizados aquí — disposición: `$format` encima) · **G-1 ✅** `aria-valuetext` (morfo Thumb `propRef('valueText')` condicional + prop `valueText?: (value, index) => string` en soma/eidos + source por-thumb; slider 10/10) · **G-2 (bloqueante, siguiente)** — DISEÑO listo tras las lecturas: espejo EXACTO de `Range` (estilo inline del provider: `left/width%` desde min hasta `secondaryValue`, RTL/vertical como `rangeStyle`) · prop `secondaryValue?: number` (unidades del dominio; extensión a par `[start,end]` DIFERIDA a clip v2, no especular) · parte morfo `SecondaryRange` optional (data como `Range`) · provider `SliderSecondaryRangeProvider` + componente soma + export `Slider.SecondaryRange` + wrapper eidos + regla `[data-slider-secondary-range]` (bajo la de range, `background: var(--slider-secondary-bg)`) + token en la sección slider de `lib/recipes/base.ts` · tests (estilo %, RTL, ausencia sin prop) | `check`: **80 errores repo (73 baseline + 7 AJENOS nuevos de la rama viva), 0 propios** · vitest slider 10/10 · sema 192/192 · days 19/19 · `morfo:vocabulary` solo warn ajeno (`cropper`) · queda al cerrar G-2: `component:audit --only slider` PASS |
| **F2 — Morfo** ✅ **COMPLETA 2026-07-31** | 6 partes audio-only `optional` (artwork sin aria-hidden — alt real del consumidor · artist · identity · transport · rate-button con `data-rate` · live-indicator decorativo) · `commit-set-rate` (target rate-button; **el commit cabalga `ratechange`** — nace con el patrón G-5, cada fuente señala una vez al aterrizar el resultado) · **`commit-set-time` RETIRADO** (morfo + pin del test + trigger de `seek()`, rationale D-AP2.13 escrito en ambos) · **`sustain-loading` CABLEADO** (`waiting`→trigger; stateBound limpia con `data-buffering`) · teclado declarado E implementado (`j`/`l` alias seek · `<`/`>` ciclan `RATE_PRESETS` [0.5…2] desde el más cercano · `0-9` salto a % solo con duration finita) · texts `rate`/`live` | ✅ morfo 8/8 + player soma 18/18 · `morfo:vocabulary` limpio (warn `cropper` ajeno) · `check` 74 = baseline, **0 propios** · ⚠️ **`translations:check` BLOQUEADO POR AJENO**: el script muere evaluando `langs/components/palabras.ts` (catálogo con `url:` https — su stripper de comentarios cercena el `//`; palabras es territorio excluido y no se toca). Mis claves siguen el patrón idlangref de las 18 hermanas; correr el verify cuando el ajeno se arregle |
| **F3 — Soma: LA MIGRACIÓN** ✅ **COMPLETA 2026-07-31** | Default del port → `uix.sound.media(el, { metadata })` (sin `uix` → sin engine; el port es la costura) · prop `metadata` en opts/types/wrapper (Active OPCIONAL) · **`nativeMediaProvider` RETIRADO** (media-provider.ts = contrato puro del puerto; exports/README al día; drift-guard borrado) · **G-5**: commits de play/mute cabalgan `play`/`pause` (saltando el pause de `ended`) y `volumechange` con transición de muted · `formatTime` → `formatDuration` · **6 sub-providers + 6 wrappers + exports** (artwork·artist·identity·transport·rate-button con `cycleRate(1)`+`data-rate`+label `rate`·live-indicator con `data-live` soma-owned) · tablas del README (props con `metadata`, 6 partes) | ✅ player 10/10 + morfo 8/8 (pin nuevo: default con motor REAL refleja `ratechange`) · prettier limpio · `check` 74 = baseline viva, **0 propios** |
| **F4 — Sema** ✅ **COMPLETA 2026-07-31** | G-4 cayó en F1 · el docblock del pack escribe la doctrina D-AP2.7 (el silencio lo posee el SERVICIO: receta `defineEngineSound({duckUiWhileContent})`; la resta-de-gain queda diseñada sin cablear a propósito) · `expression: 'pack'` intacta | `morfo:vocabulary` limpio (S11d) · stamp en navegador → F7 (este entorno no alcanza el server vivo) |
| **F5 — Eidos** ✅ **COMPLETA 2026-07-31** | **Buffer real** (G-2 consumido: `secondaryValue` + `Slider.SecondaryRange`; `.mp-buffered`/`--media-buffered`/`--_mp-rail` retirados; look intacto 42%→`--slider-secondary-bg`) · **6 wrappers eidos** (Artwork = AspectRatio+Image del catálogo vía `child`; RateButton = Button ghost con `{data-rate}×`; resto passthrough) · **`AudioLayout`** (espejo de DefaultControls: Identity[Artwork·Title·Artist·Live] + Transport[seek·play·seek] + Time·TimeSlider·Time + Rate·Mute·Volume) · **raíz competidora `<AudioPlayer>`** en `eidos/components/audio-player/` (Toast/Toaster; `metadata` DERIVADA de title/artist/artwork salvo override — la identidad que ves ES la del lock screen) · **recipe de 4 variantes** scoped a `[data-variant]` (componer a mano queda sin opinar) + tokens `--audio-player-*` (gap·artwork-radius·artwork-size-row/bar) regenerados | ✅ `recipe-css-contract` + `component-api-contract` **37/37** · `eidos-lint media-player` 0 inválidos (audio-player.css = recipe de composición sobre el morfo del player; lint per-component no la cubre — gap del tooling declarado, F7) · `check` 78 = baseline+4 AJENOS nuevos (`arts/connection`, otra sesión), **0 propios** · Notas: badge `LIVE` hardcodeado en el wrapper (el text `live` del morfo queda para cablear con langs en F7) · los internos `--_mp-*` los comparte la recipe hermana del MISMO morfo (decisión declarada; promover a públicos si se quiere estricto) |
| **F6 — Formas** | Responsive `variant`/`size` · evaluar container (D-AP2.9) | 4 variantes × light/dark × RTL × densidad **con captura y mirada** |
| **F7 — Demo + cierre** | Demo v2 (cada prop = control vivo) con `duckUiWhileContent` activo y `metadata` real · README eidos (Baseline/Comparativa/Decisiones/Gaps) · **medición Chrome: 1 contexto con earcon+media** · **prueba de teclas de medios / pantalla de bloqueo (usuario)** | `component:audit --only media-player` PASS 0 · `smoke` · `perm:check` · `check` · `docs:check` |
Método no-cascada; gates completos por fase; sin commits salvo petición.
## 3.4 Verifies `smoke` + `perm:check` de F7 (2026-07-31)
`smoke`: **301/305 rutas PASS — todas las del hilo pasan** (media-player
incluida). Los 4 fallos son rutas AJENAS al hilo: `/demos/ethereal`,
`/demos/heroscrolling`, `/demos/dome-gallery`, `/uix/components/card` —
hipótesis razonada (no diagnóstico): el árbol compartido lleva vivos cambios
de otra sesión en `eidos/components/{heading,text}/types.ts` + `lib/types.ts`
(tipografía) que Card consume. `perm:check`: 2 demos ajenas fallan (combobox,
stepper; 6/126 pasos); la demo del player no tiene `data-perm-step` aún —
opt-in pendiente anotado para el cierre del dossier. Nada del hilo en rojo;
no se toca lo ajeno.
## 3.5 Auditoría de F1–F3 (2026-07-31, previa al commit)
Clean-room sobre lo ejecutado; 2 hallazgos, ambos resueltos en el pase:
- **AU-P.1 (real, arreglado)**: el salto por dígitos (`0-9`) interceptaba
`Ctrl+1` y demás atajos del navegador — los hotkeys del provider no
comprobaban modificadores (defecto preexistente que mi adición agravaba).
Guard global: `ctrl/meta/alt` → los hotkeys no actúan (convención del sector).
- **AU-P.2 (limitación, documentada)**: `metadata` se captura al registrar el
elemento; un cambio en caliente no re-proyecta al SO. Escrito en la tabla de
props del README con su disposición (pista nueva = `src`/remount; colas fuera
por D-AP2.6; si un consumidor real lo pide, el servicio ganará
`updateMetadata` en su propia iniciativa).
Validación del conjunto: **45 suites / 458 tests verdes** (sound · sema ·
active-uix · active-app · morfo · slider · media-player · days) · `check` 74 =
baseline viva, 0 propios · `docs:check` 0 · prettier limpio en lo propio (4
ficheros del slider viejos quedan fuera de norma y NO se tocan — ajenos).
## 4. Riesgos
| Riesgo | Acotación |
| --- | --- |
| Doble transporte durante F3 | Swap atómico en el mismo pase: default nuevo + retirada del adaptador + tests actualizados juntos |
| Consumidores de `nativeMediaProvider` | Solo la demo y el propio soma (verificar con grep al abrir F3); no hay consumidores externos |
| El morfo crece (ya es el mayor) | Partes nuevas `optional`; si pasa de ~700 líneas tras F2, reabrir D-AP2.1(b) con datos |
| `duckUiWhileContent` atenúa TODA la app | Es la lectura de la incongruencia (D.7) y es opt-in de composición; si la escucha pide precisión selectiva, la variante (b) de D-AP2.7 está diseñada (v1) y no bloquea |
| Los de v1 §11 no disueltos (bar sobre fondos, clip/timeupdate…) | Siguen escritos en v1; aplican a sus fases |

Powered by TurnKey Linux.