---
estimated_steps: 47
estimated_files: 5
skills_used: []
---

# T02: Build VideoPlayer component with HLS, custom controls, and media sync hook

Build the core video player infrastructure: hls.js integration, custom playback controls, and the useMediaSync hook that shares playback state between player and transcript.

## Steps

1. Install hls.js: `cd frontend && npm install hls.js`
2. Create `frontend/src/hooks/useMediaSync.ts`:
   - Custom hook managing shared playback state: `currentTime`, `duration`, `isPlaying`, `playbackRate`, `videoRef`
   - Listens to `timeupdate` (fires ~4Hz), `play`, `pause`, `ratechange`, `loadedmetadata` events on the video element
   - Exposes `seekTo(time: number)`, `setPlaybackRate(rate: number)`, `togglePlay()` actions
   - Returns `{ currentTime, duration, isPlaying, playbackRate, videoRef, seekTo, setPlaybackRate, togglePlay }`
3. Create `frontend/src/components/VideoPlayer.tsx`:
   - Accepts props: `src: string | null`, `startTime?: number`, `mediaSync: ReturnType<typeof useMediaSync>`
   - If `src` is null, render a styled "Video not available" placeholder with explanation text
   - If src ends in `.m3u8` or device doesn't support native HLS: lazy-load hls.js via dynamic `import('hls.js')`, call `hls.loadSource(src)`, `hls.attachMedia(videoEl)`
   - If native HLS supported (Safari): set `video.src = src` directly
   - For `.mp4` URLs: use native `<video src={src}>` directly
   - Handle HLS errors: on `Hls.Events.ERROR` with `data.fatal`, log to console.error and show error state
   - `useEffect` cleanup: call `hls.destroy()` on unmount
   - If `startTime` is provided, seek to it after `loadedmetadata`
4. Create `frontend/src/components/PlayerControls.tsx`:
   - Accepts `mediaSync` prop
   - Play/pause button (toggles icon based on isPlaying)
   - Seek bar: `<input type="range">` from 0 to duration, value = currentTime, onChange seeks
   - Current time / duration display in MM:SS format
   - Speed selector: dropdown/button group with options [0.5, 0.75, 1, 1.25, 1.5, 2], active state highlighted
   - Volume control: `<input type="range">` 0–1, mute toggle button
   - Fullscreen button: calls `videoContainerRef.requestFullscreen()`
   - Keyboard shortcuts: Space = play/pause, Left/Right arrows = ±5s seek, Up/Down = ±0.1 volume
5. Add player styles to `frontend/src/App.css`:
   - `.video-player` container with 16:9 aspect ratio
   - `.video-player__unavailable` placeholder styling (dark background, centered message)
   - `.player-controls` bar styling (dark background, flex layout)
   - `.player-controls__speed` button group with active state
   - `.player-controls__seek` range input custom styling (cyan accent)
   - `.player-controls__volume` range input
   - Responsive: controls stack or resize at mobile widths
6. Verify: `cd frontend && npx tsc --noEmit` passes with zero errors.

## Must-Haves

- [ ] hls.js added to package.json
- [ ] useMediaSync hook tracks currentTime, duration, isPlaying, playbackRate
- [ ] VideoPlayer renders HLS via hls.js with Safari native fallback
- [ ] VideoPlayer shows "Video not available" when src is null
- [ ] PlayerControls has play/pause, seek, speed (0.5–2x), volume, fullscreen
- [ ] hls.js is lazy-loaded via dynamic import (not in main bundle)
- [ ] Cleanup: hls.destroy() on unmount

## Negative Tests

- Malformed inputs: null src → "Video not available" state, NaN startTime → clamp to 0
- Error paths: HLS fatal error → error message in player area, non-HLS URL → native video fallback
- Boundary conditions: duration 0 → seek bar disabled, playbackRate at limits (0.5/2x) → buttons reflect state

## Inputs

- ``frontend/src/api/client.ts` — request helper pattern`
- ``frontend/src/App.css` — existing CSS custom properties and styling patterns`
- ``frontend/package.json` — current dependencies`

## Expected Output

- ``frontend/src/hooks/useMediaSync.ts` — shared playback state hook`
- ``frontend/src/components/VideoPlayer.tsx` — HLS player component with unavailable state`
- ``frontend/src/components/PlayerControls.tsx` — custom playback controls`
- ``frontend/src/App.css` — updated with player and controls styles`
- ``frontend/package.json` — updated with hls.js dependency`

## Verification

cd frontend && npx tsc --noEmit && npm run build
