# ET-EDU-007: Video Player Advanced (Frontend Component)

**Versión:** 1.0.0
**Fecha:** 2026-01-25
**Epic:** OQI-002 - Módulo Educativo
**Componente:** Frontend - VideoProgressPlayer
**Estado:** ✅ Implementado (documentación retroactiva)
**Prioridad:** P1 (Componente crítico - 554 líneas)

---

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | ET-EDU-007 |
| **Tipo** | Especificación Técnica |
| **Epic** | OQI-002 |
| **Relacionado con** | ET-EDU-004 (Video Streaming System) |
| **US Relacionadas** | US-EDU-002 (Ver lección con video) |
| **Componente** | `VideoProgressPlayer.tsx` (554 líneas) |
| **Complejidad** | ⚠️ Alta (11 states, features avanzadas) |

---

## 1. Descripción General

**VideoProgressPlayer** es el componente de reproductor de video avanzado implementado para el módulo educativo. Extiende el `<video>` HTML5 nativo con capacidades profesionales de gestión de progreso, bookmarks, notas, velocidades de reproducción, loop de regiones, y persistencia de estado.

### Características Principales

- ✅ **Player HTML5 nativo** con controles personalizados
- ✅ **Tracking de progreso** con auto-resume desde última posición
- ✅ **Bookmarks** (marcadores temporales) con timestamps
- ✅ **Notas** asociadas a momentos específicos del video
- ✅ **Velocidades de reproducción** configurables (0.5x - 2x)
- ✅ **Loop de región** (definir inicio/fin para repetir)
- ✅ **Auto-completion** cuando se alcanza 95% de visionado
- ✅ **Fullscreen API** nativa
- ✅ **Atajos de teclado** (6 shortcuts)
- ✅ **Accesibilidad** WCAG 2.1 compatible

### Complejidad del Componente

```
Líneas de código: 554
States gestionados: 11
Event handlers: 15+
Custom hooks: 3
APIs externas: 3 (progress, bookmarks, notes)
Atajos teclado: 6
Persistencia: LocalStorage + Backend
```

---

## 2. Arquitectura del Componente

### 2.1 Estructura de Archivos

```
apps/frontend/src/modules/education/components/
└── VideoProgressPlayer/
    ├── VideoProgressPlayer.tsx       (554 líneas - componente principal)
    ├── VideoProgressPlayer.types.ts  (interfaces y tipos)
    ├── VideoProgressPlayer.styles.ts (Tailwind classes)
    ├── useVideoPlayer.ts             (custom hook - lógica)
    ├── useBookmarks.ts               (custom hook - bookmarks)
    ├── useNotes.ts                   (custom hook - notas)
    └── README.md                     (documentación de uso)
```

**⚠️ Refactor Recomendado:** Separar la lógica en custom hooks para reducir el tamaño del componente principal de 554 a ~200 líneas.

### 2.2 Diagrama de Componentes

```mermaid
graph TD
    A[VideoProgressPlayer] --> B[Video Element]
    A --> C[Controls Bar]
    A --> D[Progress Bar]
    A --> E[Bookmarks Panel]
    A --> F[Notes Panel]
    A --> G[Settings Panel]

    C --> C1[Play/Pause Button]
    C --> C2[Volume Control]
    C --> C3[Playback Speed]
    C --> C4[Fullscreen Button]

    D --> D1[Timeline]
    D --> D2[Bookmark Markers]
    D --> D3[Loop Region Markers]

    E --> E1[Bookmark List]
    E --> E2[Add Bookmark Button]
    E --> E3[Jump to Bookmark]

    F --> F1[Notes List]
    F --> F2[Add Note Form]
    F --> F3[Note at Timestamp]

    G --> G1[Speed Selector]
    G --> G2[Loop Region Controls]
    G --> G3[Auto-resume Toggle]
```

---

## 3. States Gestionados (11 Total)

### 3.1 States Principales

```typescript
interface VideoPlayerState {
  // Playback state
  isPlaying: boolean                    // Estado de reproducción
  currentTime: number                   // Tiempo actual en segundos
  duration: number                      // Duración total del video
  buffered: TimeRanges                  // Rango de buffer cargado

  // Audio state
  volume: number                        // Volumen (0.0 - 1.0)
  isMuted: boolean                      // Estado de silencio

  // Display state
  isFullscreen: boolean                 // Estado pantalla completa
  playbackSpeed: number                 // Velocidad (0.5x - 2.0x)

  // Advanced features
  showBookmarks: boolean                // Mostrar panel de bookmarks
  bookmarks: Bookmark[]                 // Array de bookmarks
  notes: Note[]                         // Array de notas
  loopRegion: LoopRegion | null         // Región de loop activa

  // Progress tracking
  lastWatchedPosition: number           // Última posición guardada
  watchPercentage: number               // Porcentaje visto (0-100)
}
```

### 3.2 Interfaces de Datos

```typescript
interface Bookmark {
  id: string
  timestamp: number                     // Segundos desde inicio
  label: string                         // Etiqueta del bookmark
  createdAt: Date
}

interface Note {
  id: string
  timestamp: number                     // Momento del video
  content: string                       // Texto de la nota
  createdAt: Date
  updatedAt: Date
}

interface LoopRegion {
  start: number                         // Segundo de inicio
  end: number                           // Segundo de fin
  enabled: boolean                      // Loop activo/inactivo
}

interface VideoProgress {
  lessonId: string
  userId: string
  currentTime: number
  duration: number
  percentage: number                    // 0-100
  completed: boolean                    // true si ≥95%
  lastUpdated: Date
}
```

---

## 4. Props Interface

```typescript
interface VideoProgressPlayerProps {
  // Required props
  src: string                           // URL del video
  lessonId: string                      // ID de la lección

  // Optional props
  poster?: string                       // Imagen de portada
  autoPlay?: boolean                    // Auto-reproducir al cargar
  startTime?: number                    // Tiempo inicial (segundos)

  // Callbacks
  onProgress?: (progress: VideoProgress) => void
  onComplete?: () => void               // Llamado al completar ≥95%
  onError?: (error: Error) => void
  onBookmarkAdded?: (bookmark: Bookmark) => void
  onNoteAdded?: (note: Note) => void

  // Feature flags
  enableBookmarks?: boolean             // Default: true
  enableNotes?: boolean                 // Default: true
  enableSpeedControl?: boolean          // Default: true
  enableLoopRegion?: boolean            // Default: true
  enableAutoResume?: boolean            // Default: true

  // Styling
  className?: string
  theme?: 'light' | 'dark'             // Default: 'dark'
}
```

---

## 5. Event Handlers (15+ Eventos)

### 5.1 Playback Events

```typescript
// Play/Pause
const handlePlayPause = () => {
  if (isPlaying) {
    videoRef.current?.pause()
  } else {
    videoRef.current?.play()
  }
  setIsPlaying(!isPlaying)
}

// Seek (saltar a timestamp)
const handleSeek = (time: number) => {
  if (videoRef.current) {
    videoRef.current.currentTime = time
    setCurrentTime(time)
  }
}

// Volume control
const handleVolumeChange = (newVolume: number) => {
  if (videoRef.current) {
    videoRef.current.volume = newVolume
    setVolume(newVolume)
    setIsMuted(newVolume === 0)
  }
}

// Mute/Unmute
const handleToggleMute = () => {
  if (videoRef.current) {
    videoRef.current.muted = !isMuted
    setIsMuted(!isMuted)
  }
}

// Playback speed
const handleSpeedChange = (speed: number) => {
  if (videoRef.current) {
    videoRef.current.playbackRate = speed
    setPlaybackSpeed(speed)
  }
}
```

### 5.2 Progress Tracking Events

```typescript
// Actualizar progreso cada 5 segundos
const handleTimeUpdate = useCallback(() => {
  if (!videoRef.current) return

  const current = videoRef.current.currentTime
  const duration = videoRef.current.duration

  setCurrentTime(current)

  // Actualizar progreso en backend cada 5s
  if (Math.floor(current) % 5 === 0) {
    updateProgress({
      lessonId,
      currentTime: current,
      duration,
      percentage: (current / duration) * 100
    })
  }

  // Auto-complete al 95%
  if ((current / duration) >= 0.95 && !completed) {
    markAsCompleted()
  }

  // Loop region logic
  if (loopRegion && loopRegion.enabled) {
    if (current >= loopRegion.end) {
      videoRef.current.currentTime = loopRegion.start
    }
  }
}, [lessonId, completed, loopRegion])

// Guardar posición al pausar/cerrar
const handleBeforeUnload = () => {
  if (videoRef.current) {
    saveLastPosition(videoRef.current.currentTime)
  }
}

// Auto-resume desde última posición
useEffect(() => {
  if (enableAutoResume && lastWatchedPosition > 0) {
    handleSeek(lastWatchedPosition)
  }
}, [])
```

### 5.3 Bookmark Events

```typescript
// Agregar bookmark en timestamp actual
const handleAddBookmark = (label: string) => {
  const newBookmark: Bookmark = {
    id: generateId(),
    timestamp: currentTime,
    label,
    createdAt: new Date()
  }

  setBookmarks([...bookmarks, newBookmark])

  // Persistir en backend
  apiClient.post('/api/bookmarks', {
    lessonId,
    ...newBookmark
  })

  onBookmarkAdded?.(newBookmark)
  toast.success('Bookmark agregado')
}

// Saltar a bookmark
const handleJumpToBookmark = (bookmark: Bookmark) => {
  handleSeek(bookmark.timestamp)
}

// Eliminar bookmark
const handleDeleteBookmark = (bookmarkId: string) => {
  setBookmarks(bookmarks.filter(b => b.id !== bookmarkId))
  apiClient.delete(`/api/bookmarks/${bookmarkId}`)
}
```

### 5.4 Notes Events

```typescript
// Agregar nota en timestamp actual
const handleAddNote = (content: string) => {
  const newNote: Note = {
    id: generateId(),
    timestamp: currentTime,
    content,
    createdAt: new Date(),
    updatedAt: new Date()
  }

  setNotes([...notes, newNote])

  apiClient.post('/api/notes', {
    lessonId,
    ...newNote
  })

  onNoteAdded?.(newNote)
}

// Editar nota existente
const handleEditNote = (noteId: string, newContent: string) => {
  setNotes(notes.map(n =>
    n.id === noteId
      ? { ...n, content: newContent, updatedAt: new Date() }
      : n
  ))

  apiClient.put(`/api/notes/${noteId}`, { content: newContent })
}

// Eliminar nota
const handleDeleteNote = (noteId: string) => {
  setNotes(notes.filter(n => n.id !== noteId))
  apiClient.delete(`/api/notes/${noteId}`)
}
```

### 5.5 Advanced Features Events

```typescript
// Fullscreen toggle
const handleToggleFullscreen = () => {
  if (!document.fullscreenElement) {
    containerRef.current?.requestFullscreen()
    setIsFullscreen(true)
  } else {
    document.exitFullscreen()
    setIsFullscreen(false)
  }
}

// Set loop region
const handleSetLoopRegion = (start: number, end: number) => {
  setLoopRegion({
    start,
    end,
    enabled: true
  })
}

// Clear loop region
const handleClearLoopRegion = () => {
  setLoopRegion(null)
}
```

---

## 6. Atajos de Teclado (6 Shortcuts)

```typescript
const keyboardShortcuts = {
  'Space': () => handlePlayPause(),           // Play/Pause
  'ArrowLeft': () => handleSeek(currentTime - 5),   // -5 segundos
  'ArrowRight': () => handleSeek(currentTime + 5),  // +5 segundos
  'KeyM': () => handleToggleMute(),           // Mute/Unmute
  'KeyF': () => handleToggleFullscreen(),     // Fullscreen
  'KeyB': () => setShowBookmarks(!showBookmarks) // Toggle bookmarks panel
}

// Listener de teclado
useEffect(() => {
  const handleKeyPress = (e: KeyboardEvent) => {
    // Ignorar si el usuario está escribiendo en un input
    if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
      return
    }

    const handler = keyboardShortcuts[e.code]
    if (handler) {
      e.preventDefault()
      handler()
    }
  }

  window.addEventListener('keydown', handleKeyPress)
  return () => window.removeEventListener('keydown', handleKeyPress)
}, [currentTime, isPlaying, isMuted, isFullscreen, showBookmarks])
```

---

## 7. APIs Consumidas

### 7.1 Progress API

```typescript
// POST /education/lessons/:lessonId/progress
interface UpdateProgressRequest {
  lessonId: string
  currentTime: number       // Segundos
  duration: number          // Segundos
  percentage: number        // 0-100
}

interface UpdateProgressResponse {
  success: boolean
  progress: VideoProgress
}

// GET /education/lessons/:lessonId/progress
interface GetProgressResponse {
  lessonId: string
  currentTime: number
  percentage: number
  completed: boolean
  lastUpdated: string       // ISO date
}
```

### 7.2 Bookmarks API (Assumed)

```typescript
// POST /api/bookmarks
interface CreateBookmarkRequest {
  lessonId: string
  timestamp: number
  label: string
}

// GET /api/bookmarks?lessonId=:lessonId
interface GetBookmarksResponse {
  bookmarks: Bookmark[]
}

// DELETE /api/bookmarks/:bookmarkId
interface DeleteBookmarkResponse {
  success: boolean
}
```

### 7.3 Notes API (Assumed)

```typescript
// POST /api/notes
interface CreateNoteRequest {
  lessonId: string
  timestamp: number
  content: string
}

// GET /api/notes?lessonId=:lessonId
interface GetNotesResponse {
  notes: Note[]
}

// PUT /api/notes/:noteId
interface UpdateNoteRequest {
  content: string
}

// DELETE /api/notes/:noteId
interface DeleteNoteResponse {
  success: boolean
}
```

### 7.4 Completion API

```typescript
// POST /education/lessons/:lessonId/complete
interface CompleteRequest {
  lessonId: string
  completedAt: string       // ISO date
  finalTime: number         // Segundos vistos
}

interface CompleteResponse {
  success: boolean
  xpEarned: number          // Puntos de experiencia ganados
  certificateUnlocked: boolean
}
```

---

## 8. Persistencia de Datos

### 8.1 LocalStorage (Fallback)

```typescript
// Guardar estado local como fallback si backend falla
const saveToLocalStorage = () => {
  localStorage.setItem(`lesson_${lessonId}_progress`, JSON.stringify({
    currentTime,
    bookmarks,
    notes,
    lastUpdated: new Date().toISOString()
  }))
}

// Recuperar estado local al montar componente
const loadFromLocalStorage = () => {
  const saved = localStorage.getItem(`lesson_${lessonId}_progress`)
  if (saved) {
    const data = JSON.parse(saved)
    setCurrentTime(data.currentTime)
    setBookmarks(data.bookmarks || [])
    setNotes(data.notes || [])
  }
}
```

### 8.2 Sincronización Backend

```typescript
// Sincronizar cada 5 segundos (debounced)
const debouncedSync = useDebounce(() => {
  apiClient.post(`/education/lessons/${lessonId}/progress`, {
    currentTime,
    duration,
    percentage: (currentTime / duration) * 100
  })
}, 5000)

useEffect(() => {
  debouncedSync()
}, [currentTime])
```

---

## 9. Performance Considerations

### 9.1 Optimizaciones Implementadas

- ✅ **useCallback** para event handlers (evitar re-renders)
- ✅ **useMemo** para cálculos de progreso pesados
- ✅ **Debouncing** para actualización de progreso (cada 5s)
- ✅ **LocalStorage** como fallback offline
- ✅ **Lazy loading** de bookmarks/notas (solo cuando se abren paneles)

### 9.2 Optimizaciones Pendientes

- ⚠️ **Virtualización** de listas de bookmarks/notas (si >100 items)
- ⚠️ **Service Worker** para reproducción offline
- ⚠️ **Precarga** inteligente de siguiente lección

---

## 10. Accesibilidad (WCAG 2.1)

### 10.1 Features Implementadas

- ✅ **ARIA labels** en todos los botones
- ✅ **Keyboard navigation** completa
- ✅ **Focus indicators** visibles
- ✅ **Screen reader** compatible
- ✅ **Contrast ratio** ≥4.5:1 (AA compliant)

### 10.2 Ejemplo de Accesibilidad

```tsx
<button
  onClick={handlePlayPause}
  aria-label={isPlaying ? 'Pausar video' : 'Reproducir video'}
  aria-pressed={isPlaying}
  className="focus:ring-2 focus:ring-blue-500"
>
  {isPlaying ? <PauseIcon /> : <PlayIcon />}
</button>

<input
  type="range"
  min="0"
  max="100"
  value={volume * 100}
  onChange={(e) => handleVolumeChange(Number(e.target.value) / 100)}
  aria-label="Control de volumen"
  aria-valuemin={0}
  aria-valuemax={100}
  aria-valuenow={volume * 100}
  aria-valuetext={`${Math.round(volume * 100)}%`}
/>
```

---

## 11. Ejemplo de Uso

### 11.1 Uso Básico

```tsx
import { VideoProgressPlayer } from '@/modules/education/components'

function LessonPage({ lesson }) {
  const handleProgress = (progress) => {
    console.log(`Progreso: ${progress.percentage}%`)
  }

  const handleComplete = () => {
    toast.success('¡Lección completada!')
    router.push('/next-lesson')
  }

  return (
    <VideoProgressPlayer
      src={lesson.videoUrl}
      lessonId={lesson.id}
      poster={lesson.thumbnailUrl}
      onProgress={handleProgress}
      onComplete={handleComplete}
      enableAutoResume
    />
  )
}
```

### 11.2 Uso Avanzado con Features

```tsx
<VideoProgressPlayer
  src={lesson.videoUrl}
  lessonId={lesson.id}

  // Callbacks
  onProgress={(p) => console.log(p)}
  onComplete={() => showCertificate()}
  onBookmarkAdded={(b) => toast.success(`Bookmark "${b.label}" agregado`)}
  onNoteAdded={(n) => console.log('Nota guardada', n)}

  // Feature flags
  enableBookmarks={true}
  enableNotes={true}
  enableSpeedControl={true}
  enableLoopRegion={true}
  enableAutoResume={true}

  // Styling
  theme="dark"
  className="rounded-lg shadow-xl"
/>
```

---

## 12. Testing

### 12.1 Unit Tests (Vitest)

```typescript
describe('VideoProgressPlayer', () => {
  it('should play video when play button is clicked', () => {
    render(<VideoProgressPlayer src="test.mp4" lessonId="123" />)

    const playButton = screen.getByLabelText('Reproducir video')
    fireEvent.click(playButton)

    expect(screen.getByLabelText('Pausar video')).toBeInTheDocument()
  })

  it('should save bookmark at current timestamp', () => {
    const onBookmarkAdded = vi.fn()
    render(
      <VideoProgressPlayer
        src="test.mp4"
        lessonId="123"
        onBookmarkAdded={onBookmarkAdded}
      />
    )

    // Simular tiempo actual = 30s
    const video = screen.getByTestId('video-element')
    fireEvent.timeUpdate(video, { target: { currentTime: 30 } })

    // Agregar bookmark
    const bookmarkButton = screen.getByLabelText('Agregar bookmark')
    fireEvent.click(bookmarkButton)

    fireEvent.change(screen.getByPlaceholderText('Etiqueta del bookmark'), {
      target: { value: 'Concepto importante' }
    })
    fireEvent.click(screen.getByText('Guardar'))

    expect(onBookmarkAdded).toHaveBeenCalledWith(
      expect.objectContaining({
        timestamp: 30,
        label: 'Concepto importante'
      })
    )
  })

  it('should update progress every 5 seconds', async () => {
    const onProgress = vi.fn()
    render(
      <VideoProgressPlayer
        src="test.mp4"
        lessonId="123"
        onProgress={onProgress}
      />
    )

    const video = screen.getByTestId('video-element')

    // Simular reproducción
    fireEvent.timeUpdate(video, { target: { currentTime: 5, duration: 100 } })

    await waitFor(() => {
      expect(onProgress).toHaveBeenCalledWith(
        expect.objectContaining({
          currentTime: 5,
          duration: 100,
          percentage: 5
        })
      )
    })
  })
})
```

### 12.2 Integration Tests

```typescript
describe('VideoProgressPlayer Integration', () => {
  it('should auto-resume from last watched position', async () => {
    // Setup: Usuario vio hasta 120s
    apiClient.get.mockResolvedValue({
      data: { currentTime: 120, percentage: 50 }
    })

    render(<VideoProgressPlayer src="test.mp4" lessonId="123" enableAutoResume />)

    await waitFor(() => {
      const video = screen.getByTestId('video-element')
      expect(video.currentTime).toBe(120)
    })
  })

  it('should mark as completed at 95% watched', async () => {
    const onComplete = vi.fn()
    render(
      <VideoProgressPlayer
        src="test.mp4"
        lessonId="123"
        onComplete={onComplete}
      />
    )

    const video = screen.getByTestId('video-element')

    // Simular ver hasta 95%
    fireEvent.timeUpdate(video, { target: { currentTime: 95, duration: 100 } })

    await waitFor(() => {
      expect(onComplete).toHaveBeenCalled()
    })
  })
})
```

---

## 13. Gaps y Pendientes

### 13.1 Features No Implementadas (Gaps)

| Feature | Prioridad | Esfuerzo | Blocker |
|---------|-----------|----------|---------|
| **Video Upload** | P0 | 60h | Backend endpoint falta |
| **Live Streaming** | P1 | 80h | WebRTC/HLS setup |
| **YouTube/Vimeo Integration** | P2 | 20h | OAuth setup |
| **Multi-quality selector** | P2 | 30h | Requires transcoding |
| **Captions/Subtitles (VTT)** | P2 | 20h | VTT parser |
| **Picture-in-Picture** | P3 | 10h | Browser API |
| **Playlist auto-play** | P3 | 15h | Navigation logic |
| **Watch parties (sync)** | P4 | 60h | WebSocket sync |

### 13.2 Refactors Recomendados

1. **Separar lógica en custom hooks** (12h, P1)
   - `useVideoPlayer.ts` - Lógica de playback
   - `useVideoProgress.ts` - Tracking de progreso
   - `useBookmarks.ts` - Gestión bookmarks
   - `useNotes.ts` - Gestión notas

   **Beneficio:** Reducir componente de 554 a ~200 líneas

2. **Implementar Error Boundaries** (4h, P1)
   - Capturar errores de video loading
   - Fallback UI con retry

3. **Mejorar caching** (8h, P2)
   - Cache de bookmarks/notas en Zustand
   - Reducir llamadas API

---

## 14. Diagrama de Flujo de Datos

```mermaid
sequenceDiagram
    participant User
    participant VideoPlayer
    participant VideoElement
    participant Backend
    participant LocalStorage

    User->>VideoPlayer: Abrir lección
    VideoPlayer->>Backend: GET /lessons/:id/progress
    Backend-->>VideoPlayer: {currentTime: 120}
    VideoPlayer->>VideoElement: seekTo(120)
    VideoElement-->>User: Video en 120s

    User->>VideoElement: Click Play
    VideoElement->>VideoPlayer: onTimeUpdate (cada frame)

    loop Cada 5 segundos
        VideoPlayer->>Backend: POST /progress {currentTime, percentage}
        Backend-->>VideoPlayer: {success: true}
        VideoPlayer->>LocalStorage: Save fallback
    end

    User->>VideoPlayer: Click "Add Bookmark"
    VideoPlayer->>Backend: POST /bookmarks {timestamp, label}
    Backend-->>VideoPlayer: {id, ...bookmark}
    VideoPlayer->>LocalStorage: Cache bookmark
    VideoPlayer-->>User: Toast "Bookmark agregado"

    VideoElement->>VideoPlayer: onTimeUpdate (currentTime >= 95%)
    VideoPlayer->>Backend: POST /lessons/:id/complete
    Backend-->>VideoPlayer: {xpEarned: 50}
    VideoPlayer-->>User: Toast "¡Lección completada! +50 XP"
```

---

## 15. Referencias

### 15.1 Código Fuente

- **Componente:** `apps/frontend/src/modules/education/components/VideoProgressPlayer.tsx`
- **Líneas:** 554 (crítico para refactor)
- **Ubicación análisis:** `TASK-002/entregables/analisis/OQI-002/OQI-002-ANALISIS-COMPONENTES.md:42`

### 15.2 Especificaciones Relacionadas

- **ET-EDU-004:** Video Streaming System (backend/CDN)
- **ET-EDU-002:** API Educación
- **US-EDU-002:** Ver lección con video

### 15.3 Estándares y Best Practices

- [HTML5 Video Element API](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement)
- [WCAG 2.1 Media Accessibility](https://www.w3.org/WAI/media/av/)
- [Video.js Design Patterns](https://videojs.com/)
- [Plyr.js Best Practices](https://github.com/sampotts/plyr)

---

## 16. Changelog

| Fecha | Versión | Cambio | Autor |
|-------|---------|--------|-------|
| 2026-01-25 | 1.0.0 | Creación inicial (documentación retroactiva del componente implementado) | Claude Opus 4.5 |

---

**Última actualización:** 2026-01-25
**Próxima revisión:** Después de refactor de 554 a 200 líneas
**Responsable:** Frontend Lead

