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

17 KiB

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 (gate v1 rechazado de raíz el 2026-07-31 por el SUSTRATO de sonido, hoy rediseñado y auditado: 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.