Design a Video Player
System design for a custom video player like YouTube: controls, progress thumbnails, quality selection, subtitles, keyboard shortcuts, fullscreen, PiP, buffering, and adaptive bitrate (HLS/DASH).
Designing a video player is a common frontend system design question that tests your grasp of media APIs, performance, accessibility, and complex UI state. Here’s a structured approach.
Requirements Clarification
Functional Requirements
- Playback: Play, pause, seek, volume, playback rate, mute.
- Progress: Scrubbing with preview thumbnails (like YouTube), jump to any position.
- Quality: Manual quality selection and auto-adaptive (ABR).
- Captions: Show subtitles, multiple tracks, user toggle.
- Layout: Fullscreen, picture-in-picture, responsive container.
- Shortcuts: Keyboard controls (space = play/pause, arrows = seek, etc.).
Non-Functional Requirements
- Low Time to First Frame (TTFF), smooth buffering UX.
- Works across major browsers and devices.
- WCAG 2.1 AA for captions and controls.
- Support HLS and DASH for adaptive streaming.
High-Level Architecture
┌─────────────────────────────────────────────────────────┐
│ VideoPlayerContainer │
├─────────────────────────────────────────────────────────┤
│ VideoElement (native <video>) │ CustomControlsOverlay │
│ - HLS.js / dash.js instance │ - ProgressBar │
│ - MediaSource API │ - ControlBar │
│ │ - CaptionsLayer │
└─────────────────────────────────────────────────────────┘
Data flow: user actions → control handlers → video element / HLS.js; media events (timeupdate, loadedmetadata, etc.) → state → UI.
Component Design
Core Components
VideoElementWrapper: Wraps <video> and manages HLS.js or dash.js. Handles src changes and exposes a stable API for play/pause/seek/volume.
ProgressBar: Shows current time, buffered range, and scrubbing. Renders preview thumbnails on hover (VTT sprite sheet or per-segment images).
ControlBar: Play/pause, volume, current time, duration, playback rate, quality menu, captions toggle, fullscreen, PiP.
CaptionsLayer: Overlays cue text using the WebVTT API (VTTCue, TextTrack). Syncs with currentTime.
interface VideoPlayerProps {
src: string; // URL or HLS/DASH manifest
poster?: string;
onTimeUpdate?: (time: number) => void;
qualityLevels?: QualityLevel[]; // from HLS/DASH
captions?: SubtitleTrack[];
}
State Management
| State | Location | Notes |
|---|---|---|
isPlaying, currentTime, duration | Component state or store | Synced from video events |
buffered | Derived from video.buffered | Ranges for progress bar |
volume, muted | Local state or persisted | LocalStorage for preferences |
qualityLevel, playbackRate | Local state | User selection |
captionsVisible, activeTrack | Local state | User preference |
isFullscreen, isPiP | Local state | Document-level APIs |
Use useSyncExternalStore or a small store if controls are shared across multiple components. Prefer local state if the player is self-contained.
API Design
Client-Side Data Contracts
// HLS.js / dash.js expose quality levels
interface QualityLevel {
height: number;
width: number;
bitrate: number;
label: string; // "1080p", "720p", "Auto"
}
// VTT for captions
// WebVTT format - cues with start/end times
// Thumbnail sprite: single image + VTT mapping positions
Backend Endpoints (if applicable)
- Manifest:
GET /video/:id/manifest.m3u8(HLS) or.mpd(DASH) - Thumbnails:
GET /video/:id/thumbnails.vtt+ sprite image - Captions:
GET /video/:id/captions/:lang.vtt
Performance Considerations
- Lazy-load HLS.js/dash.js: Only when HLS/DASH URL is used.
- Thumbnail sprites: One image + VTT for seek previews instead of per-frame images.
- Debounce progress updates: Don’t re-render on every
timeupdate; throttle to ~250ms or userequestAnimationFrame. - Virtual captions: For very long videos, only render cues near
currentTime. - Adaptive bitrate: Let HLS.js/dash.js handle switching; surface quality in the UI and optionally allow manual override.
Accessibility
- Controls: All controls keyboard-focusable; support Space, Arrow keys, M (mute), F (fullscreen).
- ARIA:
role="application"for the player,aria-labelon controls,aria-valuenow/aria-valuemin/aria-valuemaxon the seek bar. - Captions: Default on when available; respect
prefers-reduced-motion. - Focus: Trap focus in fullscreen; return focus when exiting.
- Reduced motion: Respect
prefers-reduced-motionfor preview animations.
Trade-offs and Extensions
Trade-offs: Custom controls add maintenance vs. native controls. HLS.js is widely supported but adds bundle size; dash.js is heavier. Preview thumbnails need server-side generation and storage.
Extensions: Playlist/queue, chapters, analytics (watch time, drop-off), live streams with DVR, A/B testing for player layout.
Buffering and Loading States
Handle waiting, canplay, and stalling events to show buffering indicators. Display a spinner or progress indicator when video.readyState < 2. Consider showing estimated buffer time based on buffered.end() vs. currentTime to set user expectations during poor network conditions.
Analytics and Observability
Instrument the player for product and reliability insights. Track play, pause, seek, quality changes, and drop-off (when the user leaves before the end). Measure TTFF, buffering frequency, and errors (e.g. decode failures, network errors). Send events to your analytics pipeline with video ID, timestamp, and context (quality, device). Use this data to tune ABR logic, CDN strategy, and UX—e.g. preloading or default quality. Keep PII and viewing history handling consistent with your privacy policy.
Summary
A well-designed video player separates the native <video> and streaming logic (HLS/DASH) from custom controls and UI state. Use a small, explicit state model and sync it from media events; keep controls accessible and keyboard-friendly. Optimize with lazy loading, thumbnail sprites, and throttled updates. Plan for buffering, errors, and analytics so the player is reliable and measurable in production.
Related articles
- System DesignDesign a Search Results Page
System design for a search results page: filters, sorting, facets, infinite scroll vs pagination, and performance at scale.
Read article - System DesignDesign a Comments Thread
System design for nested comments: replies, pagination, real-time updates, and moderation. Frontend architecture and data shape.
Read article - System DesignDesign Autocomplete / Typeahead Search
System design for building a Google-like autocomplete search component: debouncing, caching, keyboard navigation, race conditions, and mobile considerations.
Read article - System DesignDesign a Social Media News Feed
System design for a Twitter/Facebook-style news feed: infinite scroll, virtualization, optimistic updates, real-time updates, lazy loading, and content ranking.
Read article