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.md

37 KiB

PLAN — Reproductor de sonido (AudioPlayer)

Tipo: plan de creación por fases (process — efímero, NO fuente de verdad). Fecha: 2026-07-30 · Estado: ▶️ DESBLOQUEADO 2026-07-30 — PLAN-sound-engine.md cerró (F5), así que $sound ya existe y con él la invariante de un solo AudioContext. Decisiones D-AP.1…D-AP.12 SIN FIRMAR: el gate de §4 es lo primero de F0 y no se escribe una línea de código antes. Kickoff para sesión nueva: "Lee docs/process/PLAN-audio-player.md y presenta el gate de §4."

✅ Correcciones incorporadas (sesión 2026-07-30)

Las 5 correcciones que colgaban de esta cabecera ya están en el cuerpo:

# Corrección Dónde aterrizó
1 Faltaba la reproducción de segmentos entera — y Vidstack la tiene (clipStartTime/clipEndTime), luego es paridad pendiente, no superación fila nueva en la matriz §2 · D-AP.12 · contrato §7.5 · §10
2 G-2 (pista secundaria del Slider) sube a BLOQUEANTE de v1: tres consumidores (buffer · ventana de clip · capítulos) §8 · F1
3 D-AP.11 se reescribe contra el art: su premisa («no hay motor») era falsa D-AP.11 en §4
4 Dos invariantes al contrato: prefs.sound nunca toca el volumen del contenido · el silencio de UI debe alcanzar al Slider compuesto §7.6
5 D-AP.7 deja de ser regla de cascada y pasa a ducking, capacidad del motor D-AP.7 reescrita

Antes de escribir una línea de código: presentar al usuario las decisiones de §4 y obtener firma. Lo de aquí son propuestas razonadas, no decisiones tomadas — regla A32 (component-guide.md) + §0.5 de component-audit.md ("Get the user's sign-off on scope before writing any code").

Precondición cumplida: corpus doctrinal leído completo antes de auditar (regla dura feedback_read_full_doctrine_before_auditing): README · overview · active-architecture · CANON · morfo · sema · soma(+architecture) · eidos · active-uix · active-app · packs · blocks · agent · TSC · recipe-contract · vocabularies · component-guide · completion-checklist · component-audit · demo-authoring · theming(reference/guide/notes/motion/motion-guide/channels/ gradient-finish) · los 7 RFCs · book-deviations · design-text-effects · design-chat-block · glossary · comparison · authoring · testing-and-tooling · next-features · decisions · book-map · guía histórica · veredictos/_system/ _naming/_cierre de la auditoría · eidos/components/README.md · arts/README.md.


0. Contexto — qué existe hoy y qué NO

El ecosistema ya tiene un reproductor: media-player, genérico <video> + <audio>, cerrado como PASS en la matriz de aceptación. Y desde el 2026-07-30 tiene además motor de audio propio — el art $sound, extraído de sema/chans/sound.ts, con la invariante de un solo AudioContext cerrada y medida. Eso responde una pregunta que este plan traía mal planteada (D-AP.11) y no añade trabajo al v1: el player decide cuándo engancharse, no dónde vive el motor.

Capa Estado Evidencia
morfo 534 líneas · 17 partes · 12 eventos · expression: 'pack' · apg: 'none — …' morfo/components/media-player.ts
soma provider request-driven + puerto MediaProvider + adaptador nativo + 14 wrappers + test 9/9 soma/components/media-player/
sema pack «voz de transporte contenida» (3 reglas) sema/components/media-player.ts
eidos recipe Lumière (11.7 KB) + 17 wrappers + DefaultControls eidos/components/media-player/
demo 9 pestañas, grupo Media del sidebar web/routes/uix/components/media-player/+page.svelte

El modo audio existe, pero es un reproductor de vídeo degenerado. Todo el soporte «audio» del catálogo son tres reglas CSS:

media-player.css:79   [data-media='audio'] [data-media-player-media]    → height: 0
media-player.css:161  [data-media='audio'] [data-media-player-title]    → position: static
media-player.css:191  [data-media='audio'] [data-media-player-controls] → position: static

Es decir: se colapsa el vídeo a cero y se desabsolutizan dos cajas. No hay nada de lo que hace que un reproductor de sonido sea un reproductor de sonido: identidad de la pieza (carátula · título · artista), waveform, velocidad de reproducción visible, capítulos, formas compactas, integración con el SO.

0.1 Hallazgos de la auditoría del estado actual (evidencia leída)

ID Hallazgo Evidencia Consecuencia
H-1 Regla sema probablemente muerta. El pack construye su selector sobre provider (semaSelector(morfo,'provider',{eventName:'commit-toggle-play'}) → [data-media-player][data-event='commit-toggle-play']) pero el evento estampa en su propio botón (target: v.partRef('play-button')). sema/components/media-player.ts:23 vs morfo/…/media-player.ts:95 La firma de play/pause nunca se aplicaría. Verificar en F0 con stamp real en navegador; si se confirma, es fix de una línea (onPlayButton).
H-2 SettingsButton y Captions declarados e inertes ("render inertly until implemented", README soma:99-100). soma/…/README.md:96 El menú velocidad/pista es justo lo que más pesa en audio (podcast). Deuda declarada que este plan recoge.
H-3 Slider no tiene pista secundaria. Partes: Provider · Range · Thumb · Tick. El buffer del player es un div eidos-only que lee --media-buffered. morfo/components/slider.ts Un waveform-seek necesita el mismo hueco. Gap de framework (§8).
H-4 Slider no declara aria-valuetext. Solo aria-valuemin/max/now. morfo/components/slider.ts:107-109 El lector de pantalla anuncia «127», no «2:07 de 41:15». Fallo de nivel de referencia en el control MÁS importante del player.
H-5 Formato de tiempo hecho a mano. formatTime() con padStart local, fuera de uix.format / $libs/days. soma/…/media-player-provider.svelte.ts:34-40 Contradice el eje de servicios (i18n · dígitos no arábigos · RTL).
H-6 No hay --media-progress. Se eliminó por código muerto; solo sobrevive --media-buffered. README eidos:47 Un waveform necesita el progreso como var continua otra vez (o su propio mecanismo). Decisión, no accidente.
H-7 Cero integración MediaSession en todo el repo. grep -rn mediaSession src/ = 0 Sin controles de pantalla de bloqueo / teclas de medios del SO.
H-8 La carátula (Poster) es position:absolute; inset:0; object-fit:cover sobre una caja 16/9. En audio esa caja no existe (media a height:0). media-player.css:85-93 La carátula de un álbum no tiene hogar geométrico hoy.

Ninguno de los ocho invalida el media-player: son exactamente la superficie que un modo audio de nivel de referencia obliga a cerrar.


1. Qué es —de verdad— un reproductor de sonido

No es «un player sin imagen». Cambian tres cosas de raíz:

  1. La superficie deja de ser el contenido. En vídeo el marco es la obra y el chrome se esconde encima (scrim, auto-hide). En audio no hay marco: la UI es lo único visible, así que pasa de transparente a persistente, y su trabajo cambia de «no estorbar» a «identificar la pieza».
  2. La identidad sustituye al fotograma. Carátula, título, artista/serie, capítulo. Ninguna existe hoy más allá de un Title de una línea.
  3. La línea de tiempo se vuelve legible. En vídeo el scrubber se previsualiza con miniaturas; en audio la referencia del sector es la onda (SoundCloud, wavesurfer) o los capítulos (podcast). La barra desnuda es el mínimo, no el estándar.

Y aparecen tres afordancias que en vídeo son secundarias y aquí son de primera fila: velocidad (podcast/curso), ±15/30 s (ya existe como SeekButton) y continuidad con el SO (pantalla de bloqueo, auriculares, coche).


2. Comparativa contra referencias (F-1.2 exige ≥3; aquí 8 + la plataforma)

Consultado 2026-07-30. La columna «UIX hoy» = media-player en media="audio".

Capacidad Vidstack (DefaultAudioLayout) Media Chrome (<media-controller audio>) Plyr Red Hat rh-audio-player wavesurfer.js 7 react-h5-audio-player UIX hoy Veredicto v1
play/pause · ±seek · mute · volumen · tiempo ✅ ✅ ✅ ✅ ✅ ✅ ✅ —
Layout de audio propio (no vídeo colapsado) ✅ DefaultAudioLayout ✅ atributo audio ✅ preset audio ✅ layout n/a ✅ ❌ 3 reglas CSS implementar
Formas / tamaños (full · compact · mini · barra) ⚠️ smallLayoutWhen (2 tamaños) ⚠️ composición manual ❌ ✅ full/compact/mini n/a ⚠️ ❌ implementar
Identidad: carátula · título · artista ⚠️ Title+Poster ⚠️ slots ⚠️ ✅ series/title/poster ❌ ✅ header ⚠️ solo Title implementar
Velocidad de reproducción (UI) ✅ menú + SpeedSlider ✅ media-playback-rate-button ✅ settings.speed ✅ n/a ❌ ❌ (setPlaybackRate existe, sin UI) implementar (cierra H-2)
Waveform ❌ ❌ ❌ ❌ ✅ (el núcleo) ❌ ❌ componente propio (D-AP.4)
Segmentos (reproducir start…end de una fuente) ✅ clipStartTime / clipEndTime (recalcula el rango seekable) ? ? ? ⚠️ Regions (plugin) ? ❌ implementar v1 (D-AP.12)
Capítulos ✅ SliderChapters + ChapterTitle + radio-group ⚠️ vía cues ✅ markers ✅ transcript/cues ✅ Regions ❌ ❌ diferir v2
Transcripción ⚠️ captions ⚠️ ❌ ✅ rh-transcript+rh-cue ❌ ❌ ❌ diferir v2
Cola / playlist ❌ ❌ ❌ ❌ ❌ ⚠️ skip prev/next ❌ fuera (D-AP.6)
MediaSession (SO) ❌ ❌ ❌ ❌ ❌ ❌ ❌ superación (D-AP.5)
Ganancia >100 % ✅ AudioGainSlider ❌ ❌ ❌ ⚠️ Envelope ❌ ❌ diferir v2
Loop / descarga ⚠️ ⚠️ ✅ ⚠️ n/a ✅ loop ⚠️ nativo loop v1 (prop), descarga = app
Teclado completo ✅ ✅ ✅ ✅ ⚠️ ✅ ✅ (9 teclas) ✅
aria-valuetext hablado en el scrubber ✅ ✅ ✅ ✅ ❌ ⚠️ ❌ (H-4) gap de framework (§8)
Motor enchufable (HLS/DASH/embed) ✅ ✅ ✅ ❌ ❌ ❌ ✅ puerto MediaProvider ✅ ya por encima
Semántica perceptiva (familia · intent · sonido/háptica) ❌ ❌ ❌ ❌ ❌ ❌ ✅ pack sema ✅ exclusivo
Contrato declarativo verificable (morfo + guards) ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ exclusivo

Lectura. El transporte ya está a la altura (y el puerto de motor, por encima). Lo que falta es todo lo que distingue audio de vídeo: layout propio, formas, identidad, velocidad, onda, segmentos. Dos capacidades que ninguna referencia tiene ya son nuestras (sema + morfo), y una que ninguna de las ocho implementa —MediaSession— es la palanca barata de superación.

Las celdas ? de la fila Segmentos están sin verificar: sólo se comprobó Vidstack (2026-07-30). Cerrarlas es tarea de F0 — y no cambian el veredicto, porque con una referencia que lo trae ya es paridad pendiente.

Fuentes: Vidstack components · Vidstack Default Layout · Media Chrome · Plyr · Red Hat audio player · wavesurfer.js · react-h5-audio-player · MDN MediaSession · web.dev Media Session · W3C WAI media players.


3. La tensión arquitectónica (leerla antes de las decisiones)

Dos reglas del propio proyecto apuntan en direcciones opuestas:

  • P-4 (norma de pickers, architecture/eidos.md): «There are no per-variant components (MonthPicker, HourPicker). Those forms are <X kind='month'>». → un reproductor de audio es <MediaPlayer media="audio">.
  • Regla de admisión (packs/blocks): lo que tiene superficie de contrato propia se promueve a componente canónico y se compone. → la onda, el menú de velocidad y la identidad podrían querer casa propia.

La resolución no la dan las reglas sino las dos referencias primarias, que coinciden: Vidstack mantiene UN <media-player> y cambia el layout (DefaultAudioLayout); Media Chrome mantiene UN <media-controller> y cambia la composición de la barra (atributo audio). Ninguna bifurca el motor.

Y el catálogo ya tiene el precedente exacto para «una segunda raíz visual sobre el mismo contrato headless»: Toast / Toaster — «two independent roots (not nested)… Toaster is NOT attached as Toast.Toaster because it is a competing root, not a child» (architecture/eidos.md §Special case). DefaultControls ya es, hoy, una composición eidos-only del mismo tipo.


4. Decisiones (gate del usuario — SIN FIRMAR)

ID Cuestión Opciones Recomendación
D-AP.1 Pertenencia: ¿componente nuevo o modo del existente? (a) un motor, un contrato: media-player conserva morfo/soma/sema y gana partes audio-only optional: true + un layout eidos · (b) audio-player nuevo que compone media-player (precedente picker/A27) · (c) fork con su propio provider (a). Duplicar transporte + puerto + teclado + eventos es exactamente la reinvención que el §4 del component-guide prohíbe; las dos referencias primarias hacen (a); el morfo YA declara data-media: ['video','audio'] y ya marca optional: true todas las partes no-núcleo (PipButton/FullscreenButton son video-only con ese mismo mecanismo — no hace falta inventar nada). (b) añade una capa de indirección sin contrato nuevo.
D-AP.2 Puerta pública: ¿cómo se pide un reproductor de sonido? (a) <MediaPlayer media="audio"> + <MediaPlayer.AudioLayout/> (espejo de DefaultControls) · (b) raíz competidora <AudioPlayer> en eidos sobre el mismo soma (precedente Toast/Toaster) · (c) ambas (c): AudioLayout como parte eidos-only (composición explícita, coherente con DefaultControls) y <AudioPlayer> como raíz competidora exportada aparte —no como MediaPlayer.AudioPlayer— que la monta con media="audio". El 90 % del uso es una etiqueta; el 10 % compone partes. Coste morfo/soma/sema: cero.
D-AP.3 Formas y variaciones (petición explícita del usuario) (a) solo size canónico · (b) variant propio de componente · (c) variant + size + responsive (c). variant: 'card' | 'row' | 'bar' | 'inline' como variante específica de componente —vía legítima y con precedente escrito (Banner: inline|overlay|persistent, Spinner: bars|dots|ring, theming §19)— declarada en su types.ts, NO en EIDOS_VARIANTS. size = subconjunto canónico xs..xl (la recipe ya lo mapea). Ambas admiten ResponsiveProp<T>. Detalle en §6.
D-AP.4 Waveform (a) componente canónico Waveform propio (ruta de 9 fases) · (b) parte eidos-only del player · (c) fuera (a), pero en su propia iniciativa, DESPUÉS del v1. Tiene superficie de contrato real (es un role="slider" con teclado, data-* y eventos handle-*), y sirve fuera del player (nota de voz en chat-message, editor). Meterlo como parte eidos-only sería crecerle el privilegio al player — el antipatrón que la regla de admisión existe para impedir. Los picos: peaks: number[] que aporta la app (ruta profesional: BBC audiowaveform/SoundCloud) con decodificador cliente opcional; el renderer reutiliza $libs/plots (scaleLinear + area/line), no inventa matemáticas.
D-AP.5 MediaSession (SO) (a) prop opt-in en el provider soma · (b) art nuevo $media-session · (c) app-land (a) opt-in, apagado por defecto, mediaSession={{ title, artist, album, artwork }}. Es exactamente el snapshot que el provider ya posee (playbackState, setPositionState, seekto), y dejarlo fuera obliga a cada consumidor a re-derivarlo. Riesgo declarado: navigator.mediaSession es singleton de documento — dos players activos se pisan; el provider debe registrar solo mientras !paused y liberar en pause/dispose. (b) es sobreingeniería para una API sin estado propio. Ninguna de las 8 referencias lo trae → superación barata.
D-AP.6 Cola / playlist (a) en el player · (b) block/app-land (b), fuera de alcance. Una cola es una composición de Listbox + estado de aplicación: cero contrato nuevo → tier blocks o app. El player expone commit-complete y ya. Se registra en next-features.md.
D-AP.7 Firma sema en modo audio — reescrita 2026-07-30 (corrección 5) (a) heredar el pack actual · (b) silenciar el canal sonoro en audio · (c) silencio mientras suena + ducking cuando no basta (c). El suelo sigue siendo (b) y su doctrina no cambia: D.7 (book-deviations) «En eventos frecuentes, la prioridad es evitar fatiga. El silencio es una firma válida» + antipatrón cap. 34 §10 «sonido decorativo» — si el contenido ES audio, un tick de UI compite con la obra. Regla del pack: selector [data-media-player][data-media='audio'] → channels: ['haptic'], que añade carácter sin tocar las primitivas del intent. Lo que cambia es que el silencio ya no tiene que ser total ni de cascada: $sound es dueño del earcon, así que atenuar la UI mientras suena la obra no necesita que el player consuma el grafo — basta con que el provider baje el master (uix.sound.setMasterGain) mientras !paused y lo restaure al pausar. Regla de reparto: la cascada decide cuándo callar, el motor decide cuánto. Lo que sí exige el grafo (D-AP.11(b), diferido) es lo contrario: agachar el contenido bajo un earcon, o hacerlo con precisión de muestra. A firmar con la decisión: el master es global, así que hay que decidir quién lo restaura y qué pasa con dos players a la vez — si no se resuelve, el v1 se queda en (b) puro, que ya es correcto.
D-AP.8 Capítulos + transcripción (a) v1 · (b) v2 · (c) fuera (b) v2, registrado con disposición en el README (F-1.4). Capítulos = pista secundaria del scrubber → depende de D-AP.4/H-3; transcripción = superficie de contenido con su propia a11y (candidata a componente propio, patrón Red Hat).
D-AP.9 Responsive del layout (a) ResponsiveProp (mecanismo del framework) · (b) container queries del TSC · (c) ambos (a) en v1, (b) evaluado en F6. El TSC ya tiene el eje container: {} documentado como «a themeable axis with 0 consumers today — an open cage» (canon/tsc.md): un player que vive dentro de un aside, una tarjeta o un pie de página es el primer consumidor natural del catálogo, y resolvería mejor que el smallLayoutWhen de Vidstack (que depende del contenedor, no del viewport). Abrir la jaula es un entregable con valor propio — pero no debe bloquear el v1.
D-AP.10 Alcance del v1 (a) paridad estricta con las referencias · (b) paridad + superación (MediaSession + silencio sema + container) (a) + MediaSession + silencio sema + segmentos; container y waveform como iniciativas hermanas. Criterio: el usuario pidió «como mínimo al mismo nivel» — la paridad es el suelo, no el techo, y las dos superaciones baratas caben. Los segmentos entran por paridad (D-AP.12), no por ambición.
D-AP.11 ¿El player consume $sound? — escrita 2026-07-30 contra el art ya existente; su premisa anterior («no hay motor, ¿dónde lo ponemos?») era falsa (a) no consumirlo en v1: el player sigue sobre <audio> nativo y $sound sólo suena la UI · (b) consumirlo desde v1: colgar el MediaElementAudioSourceNode de uix.sound.context · (c) consumirlo sólo cuando el consumidor lo pida (prop opt-in) (a) en v1, (b) como iniciativa hermana con Waveform. El art existe y la invariante de contexto único está cerrada y medida (PLAN-sound-engine.md §15), así que la pregunta ya no es dónde vive el motor sino cuándo engancharse. Enganchar <audio> a Web Audio no es gratis: createMediaElementSource redirige el audio del elemento al grafo de forma irreversible, y con una fuente cross-origin sin CORS lo deja MUDO. Un v1 cuyo trabajo es layout, identidad, formas, velocidad y segmentos no necesita el grafo; quien lo necesita es la onda, el visualizador, la ganancia >1 y el ducking del contenido — todos diferidos. Consecuencia declarada: con (a), el ducking de D-AP.7 sólo puede atenuar la UI (setMasterGain), nunca la obra; y el punto de bucle del clip (D-AP.12) sigue atado a timeupdate. Lo que sí entra en v1 es la regla escrita: si algún día se engancha, es a uix.sound.context, jamás a un new AudioContext() propio.
D-AP.12 Segmentos (clip) — fila nueva 2026-07-30 (corrección 1); es paridad, Vidstack los trae (a) fuera de v1 · (b) clip = { start, end, loop? } en el provider · (c) regions genéricas (n segmentos con estado propio) (b), con este diseño ya acordado: capa de proyección absoluto↔relativo (dominio del slider 0…end−start, timeBase: 'clip' | 'source' en Time, misma base en aria-valuetext) · clamp en el provider, no en el puerto (el puerto MediaProvider no se contamina: sigue hablando de la fuente entera) · commit-complete dispara en el fin del clip — mismo evento, frontera movida · con loop NO hay commit-complete: un bucle esperado no es una ocurrencia (regla B.5). Limitación declarada: timeupdate llega ~4 veces/s, así que sin Web Audio el punto de bucle es impreciso (~±125 ms) — se documenta, no se disimula. Riesgo a probar en F0: bug abierto de clip en Chrome/Safari móvil (vidstack#1195). (c) queda descartado y chapters v2, los dos por depender de G-2.

5. Arquitectura propuesta (tras firma de D-AP.1/2)

morfo/components/media-player.ts     +6 partes optional (audio-only) · +2 eventos · +1 texto
        │                             (contrato ÚNICO — no se bifurca)
soma/components/media-player/        +sub-providers de las partes nuevas
        │                             +MediaSession opt-in en el provider raíz
        │                             +clip {start,end,loop} + proyección (D-AP.12)
        │                             +formatTime → uix.format (H-5)
sema/components/media-player.ts      +regla de silencio en audio (D-AP.7) · fix H-1
        │
eidos/components/media-player/       +audio-layout.svelte (composición, espejo de DefaultControls)
        │                             +recipe: variantes card/row/bar/inline
eidos/components/audio-player/       raíz competidora <AudioPlayer> (Toast/Toaster)

Lo que NO se toca: el puerto MediaProvider (ya es el acierto del componente), el modo vídeo, el recipe Lumière existente, la demo de media-player.

Dogfooding obligatorio (§4 component-guide): la barra compone IconButton (ya lo hace), los sliders componen Slider (ya), el menú de velocidad compone DropdownMenu, la carátula compone Image + AspectRatio, la identidad compone Text/Heading, el estado vacío compone EmptyState. Cero primitivas inline.


6. Formas y variaciones (el punto 3 del encargo, en detalle)

Cuatro formas, un solo contrato. La variante decide qué partes monta el layout y cómo se disponen, nunca qué puede hacer el player.

variant Forma Partes que monta Contexto de uso Referencia del sector
card Tarjeta vertical: carátula grande arriba, identidad, scrubber, transporte centrado Artwork · Title · Artist · TimeSlider · Time×2 · Play · Seek± · Mute · Volume · Rate Página de episodio, ficha de álbum, landing Apple Podcasts / Bandcamp
row Fila horizontal: carátula pequeña a la izquierda, identidad + scrubber apilados Artwork(sm) · Title · Artist · TimeSlider · Play · Seek± · Rate Lista de episodios, resultado de búsqueda, feed SoundCloud track
bar Barra persistente a ancho completo (composición pensada para sticky/pie) Artwork(xs) · Title · Play · Seek± · TimeSlider · Time · Mute · Volume · Rate Reproductor global de la app Spotify bottom bar
inline Mínima, en línea con el texto: play + scrubber + tiempo Play · TimeSlider · Time Nota de voz en un chat, cita de audio en un artículo mensaje de voz de WhatsApp

Ejes ortogonales que componen con la variante:

  • size (xs..xl, ResponsiveProp) — densidad; la recipe ya mapea el bundle --size-{k}-*. Regla dura: md no cambia de significado por viewport (theming §5); lo que cambia es qué size se elige.
  • color — el sistema completo (rol / intent / 33 escalas / CSS crudo = ComponentColorProp), sin subconjuntos: decisión «abrir la jaula del color» (theming §25, guard en recipe-css-contract.test.ts).
  • variant responsive — variant={{ base: 'inline', md: 'row', lg: 'card' }}.
  • data-depth — la barra persistente estampa su plano (raised/overlay), no inventa sombras (contrato de recipe R-4.1).

Regla de forma (espejo de la que rige blocks): la variante no puede añadir comportamiento. Si una forma necesitase un evento, un data-* que el CSS deba seleccionar o una obligación a11y nueva, eso se promueve al contrato —no se le crece el privilegio al layout.


7. Contrato propuesto (borrador para F1 — se cierra con la firma)

7.1 Partes nuevas (todas optional: true, audio-only)

Parte kebab archetype Elemento Rol
Artwork artwork image img Carátula. Compone Image+AspectRatio; alt real (no decorativa como Poster, porque en audio es la única representación visual de la obra).
Artist artist description div Autoría / serie.
RateButton rate-button trigger button Abre el menú de velocidad (compone DropdownMenu) o cicla el preset. Cierra H-2 junto a SettingsButton.
Identity identity group div Agrupador de Artwork+Title+Artist (le da a la variante un bloque que mover).
Transport transport group div Agrupador de los botones de transporte, hermano de Controls.
LiveIndicator live-indicator indicator div Emisión en directo (data-live ya existe en el provider).

7.2 Eventos nuevos

Evento Familia · verbo Intent Target sequence Por qué
commit-set-rate commit · set neutral rate-button post Fijar velocidad es un valor aplicado (commit.set, BOOK_CANON A.5: «un valor, criterio o parámetro ha quedado aplicado» — mismo caso que sort/slider).
sustain-end sustain · end — provider coincident Cierre del proceso de buffering. Solo si F0 confirma que sustain-loading (persistence: 'stateBound') no se limpia solo; si se limpia, no se declara (regla B.5: un cambio no perceptible no es evento).

Nada más. loop es prop nativa sin evento (B.5). El cambio de carátula/título es dato, no ocurrencia.

7.3 Teclado

Se hereda el contrato del provider (9 teclas). Se añaden, alineadas con el sector (j/l de YouTube, </> de velocidad):

Tecla Acción Nota
j / l seek ∓ seekStep alias de ←/→, convención del sector
< / > bajar / subir velocidad requiere RateButton montado
0–9 saltar al 0–90 % solo cuando duration es finita (no en directo)

7.4 A11y — el listón

  • aria-valuetext hablado en el scrubber (cierra H-4): «2 minutos 7 segundos de 41 minutos», no «127». Es gap de framework (§8).
  • Artwork con alt real; el resto de la identidad como texto real (nunca aria-label sobre divs vacíos).
  • commit-fail ya declara requiresLiveRegion + reducedMotionFallback: 'text' — se conserva.
  • Contraste: la barra bar sobre superficie arbitraria consume data-depth, no color inventado (R-2.1/R-4.6).
  • Sin robo de foco al cambiar de pista.

7.5 Segmentos (clip) — el contrato (D-AP.12)

Pieza Regla
Prop clip={{ start, end, loop? }} en el provider soma. Ausente = fuente entera (comportamiento de hoy, sin cambios)
Puerto MediaProvider no se toca: sigue hablando de la fuente entera. El clamp y la proyección viven en el provider
Dominio del slider 0…end−start. Un scrubber que empieza en 0 es la única lectura honesta para quien no sabe que hay una fuente mayor detrás
Time timeBase: 'clip' | 'source' — y aria-valuetext usa la misma base que el Time visible. Dos bases distintas en la misma pantalla es un fallo de a11y, no una opción
Fin commit-complete dispara al llegar a end — el mismo evento, la frontera movida
loop NO dispara commit-complete: un bucle esperado no es una ocurrencia (B.5)
Pista secundaria La ventana del clip se pinta en la pista secundaria de Slider — G-2, uno de sus tres consumidores
Limitación timeupdate llega ~4 veces/s: sin Web Audio el punto de bucle es impreciso (~±125 ms). Se declara en el README, no se disimula

7.6 Dos invariantes que este componente es el primero en necesitar

  1. prefs.sound NUNCA toca el volumen del contenido. Gobierna el sonido de UI (sema: earcons, off / reduce). Un reproductor de sonido es el primer componente del catálogo donde «sonido» significa dos cosas, y confundirlas silenciaría la obra al bajar una preferencia de interfaz. El volumen del contenido es volume / muted del provider, y nada más.
  2. El silencio de UI en audio (D-AP.7) tiene que alcanzar al Slider compuesto. Existe resolveSliderDragSound: arrastrar el scrubber sonorizaría por encima de la obra. Verificar con stamp real en navegador (F0) y, si dispara, la regla es [data-media-player][data-media='audio'] [data-slider].

8. Gaps de framework a promover ANTES (regla de admisión)

Se construyen primero, en el canon, y el player los compone. Ninguno es un parche local del player.

# Gap Dónde Justificación 2-de-3 Coste
G-1 aria-valuetext en el morfo de Slider (v.propRef('valueText')) + prop valueText en soma morfo/components/slider.ts + provider aria es campo morfo por definición (soma emite, eidos puede seleccionar) bajo
G-2 🔴 BLOQUEANTE de v1 Pista secundaria («buffered»/«secondary range») en Slider morfo + soma + recipe Lo consumen soma (valor) y eidos (pintura) → 2-de-3 ✓. Y ya tiene tres consumidores, no uno: el buffer, la ventana del clip (D-AP.12) y los capítulos (v2). Hoy lo simula un div eidos-only en media-player y lo simularía otro en Waveform — dos copias = la señal de que falta la pieza; tres = que bloquea medio
G-3 formatDuration en $libs/days (o uix.format) $libs/days Regla A23: si es puro y reutilizable, va a la lib — nunca reimplementar dentro de soma bajo
G-4 Verificar/arreglar H-1 (selector del pack sema) sema/components/media-player.ts fix de una línea si se confirma trivial

Waveform (D-AP.4) no está en esta tabla: no es un gap del player sino una iniciativa hermana con su propia ruta de 9 fases.


9. Fases (tras el gate)

Fase Contenido Verify
F0 — Verificación y firma Confirmar H-1 (stamp real en navegador) · confirmar si el Slider compuesto sonoriza al arrastrar (7.6.2) · confirmar si sustain-loading se auto-limpia (decide 7.2) · cerrar las celdas ? de la fila Segmentos (§2) · reproducir vidstack#1195 en Chrome/Safari móvil (decide si el clamp del clip necesita defensa propia) · leer DefaultControls, Slider provider y la recipe del scrubber ANTES de diseñar (regla feedback_study_real_recipes_before_building) · presentar §4 y §6 al usuario y firmar D-AP.1…D-AP.12 tabla de decisiones firmada en el chat; este plan actualizado
F1 — Gaps de framework G-1 · G-2 (bloqueante) · G-3 · G-4, cada uno con su test check 0 propios · vitest del slider verde · component:audit --only slider PASS · morfo:vocabulary exit 0
F2 — Morfo 6 partes + 1–2 eventos + texts + catálogo langs/components/media-player.ts validateMorfo · test del morfo · morfo:vocabulary · translations:check
F3 — Soma sub-providers de las partes nuevas · MediaSession opt-in (con guarda de singleton) · clip + la proyección absoluto↔relativo + el clamp (§7.5) · formatTime→G-3 · tests del provider test del provider verde (incl. clip: dominio, commit-complete en end, silencio del evento con loop) · check 0 propios
F4 — Sema regla de silencio en audio (D-AP.7) + fix H-1; expression sigue 'pack' morfo:vocabulary (coherencia pack↔expression, guard S11d) · stamp verificado en navegador
F5 — Eidos audio-layout.svelte + raíz <AudioPlayer> + recipe de las 4 variantes + la ventana del clip pintada en la pista secundaria de G-2 + tokens --audio-player-* en lib/recipes/base.ts generate:eidos-css (TSC) · recipe-css-contract · eidos-lint 0 inválidos · component-api-contract
F6 — Formas responsive de variant/size; evaluar container queries (D-AP.9) prueba visual de las 4 variantes × light/dark × RTL × densidad, con captura y mirada (regla dura feedback_verify_visually_before_showing)
F7 — Demo + dossier + cierre demo v2 de 9 pestañas (cada prop = control vivo, paridad de chips D-7.4) · README eidos (Baseline/Comparativa≥3/Decisiones/Gaps con disposición) · README soma actualizado · data-perm-step component:audit --only media-player PASS 0 errores · smoke · perm:check · check 0 propios · docs:check

Método: no-cascada — una pieza, verificar, la siguiente (método canonizado en _cierre.md). Gates completos al cerrar cada fase.


10. Fuera de alcance

  • Cola / playlist (D-AP.6) → blocks o app; se registra en next-features.md.
  • Waveform (D-AP.4) → iniciativa hermana, ruta de 9 fases propia.
  • regions (n segmentos con estado propio) → descartado, no diferido: es el modelo de wavesurfer y trae su propio contrato (selección, arrastre, solapamiento). clip (D-AP.12) resuelve el caso real —reproducir un trozo— sin abrirlo.
  • Enganchar el player a uix.sound.context (D-AP.11) → iniciativa hermana, con Waveform y el visualizador. El v1 no toca Web Audio.
  • Capítulos, transcripción, ganancia >100 %, ecualizador, grabación → v2, cada uno con disposición escrita en el ## Gaps del README.
  • Vendorizar motores (hls.js, Howler, wavesurfer) → el puerto MediaProvider ya es la respuesta; doctrina de cero dependencias.
  • Visualizador/espectro → si algún día se hace, su hogar es $scene (EngineScene ya resuelve frame-loop, pausa fuera de vista, cap de DPR, reduced-motion obligatoria, pérdida de contexto y presupuesto) o el tier packs — nunca un canvas suelto dentro del recipe del player.

11. Riesgos declarados

Riesgo Mitigación
El morfo de media-player ya es el mayor del catálogo; crece más Todas las partes nuevas optional: true; si tras F2 el fichero pasa de ~700 líneas, reabrir D-AP.1(b) con datos, no con intuición
MediaSession es singleton de documento Registro solo mientras suena + liberación en pause/dispose; documentado en el README como limitación conocida
La variante bar vive sobre fondos arbitrarios data-depth + el layer de estado; nunca color crudo
El clip depende de timeupdate (~4/s) El bucle es impreciso (~±125 ms) y se declara en el README; la precisión de muestra necesitaría Web Audio, que el v1 no engancha (D-AP.11)
Bug abierto de clip en Chrome/Safari móvil (vidstack#1195) Reproducirlo en F0 antes de firmar D-AP.12; si se confirma, el clamp del provider necesita defensa propia y se escribe como limitación conocida
Sobrecarga de alcance El v1 es la columna «implementar» de §2 y nada más; todo lo demás lleva disposición escrita

Powered by TurnKey Linux.