This document presents a scalable frontend architecture for video streaming interfaces, covering component hierarchies, state management patterns, and performance optimizations essential for modern media platforms.
1. Problem Statement & Requirements
Clarifying questions
- What are the target devices/browsers for this video streaming UI? (e.g., mobile-first, desktop-only, TV support)
- Are there specific authentication requirements? (e.g., OAuth, SSO, guest access, social login)
- Should the UI support real-time features like live streaming or chat, or is it primarily static content playback?
- Is offline support required? (e.g., video caching, download capability, offline mode)
- What level of accessibility compliance is required? (e.g., WCAG 2.1 AA, full keyboard navigation)
- Is i18n support needed for multiple languages or regions?
- Are there existing design systems or component libraries that must be used?
Functional requirements
- Users can browse and search for videos by category, title, or tags
- Video playback with controls (play/pause, volume, fullscreen, progress bar)
- Responsive design supporting multiple screen sizes
- Video metadata display (title, description, duration, upload date)
- Playlist support with next/previous navigation
- User preferences (watch history, favorites, playback settings)
- Video recommendations based on user behavior or content similarity
- Multi-quality streaming (HD, SD, adaptive bitrate)
- Thumbnail previews for video lists and search results
- Video progress tracking and resume capability
Non-functional requirements
- Performance: LCP < 2.5s, CLS < 0.1, INP < 200ms for interactive elements
- Accessibility: WCAG 2.1 AA compliance with full keyboard navigation and screen reader support
- Scalability: Support for 10K+ concurrent users with efficient content delivery
- Reliability: 99.9% uptime for core video playback functionality
- Security: Secure content delivery with proper DRM support and XSS prevention
Constraints and assumptions
- Video content is served via CDN with adaptive bitrate streaming (HLS/DASH)
- Authentication handled by separate identity service
- UI must support progressive enhancement for older browsers
- No server-side rendering required (client-side only)
- Video player component must be compatible with various video formats
- Content metadata is assumed to be available via API
Out of scope
- User registration and account creation flows (handled by separate auth system)
- Video upload functionality or content management tools
- Third-party ad integration or monetization features
- Real-time collaborative features (chat, comments, live reactions)
- Offline video download capabilities
- Analytics tracking and user behavior measurement (separate monitoring system)
- Custom video player UI beyond standard controls (e.g., custom skins, overlays)
2. Component Architecture
graph TD
A[VideoStreamingApp] --> B[Header]
A --> C[MainContent]
A --> D[Footer]
C --> E[VideoPlayer]
C --> F[VideoList]
C --> G[VideoDetails]
E --> H[PlayerControls]
E --> I[VideoMetadata]
E --> J[VideoQualitySelector]
F --> K[VideoCard]
F --> L[VideoGrid]
F --> M[SearchBar]
G --> N[VideoDescription]
G --> O[CommentsSection]
G --> P[RelatedVideos]
K --> Q[Thumbnail]
K --> R[VideoTitle]
K --> S[VideoDuration]
L --> T[VideoRow]
L --> U[VideoColumn]
Component Tree Breakdown
Smart Components (Container Components):
VideoStreamingApp- Main app container with routing and global stateMainContent- Layout container for main content areaVideoPlayer- Player component with playback state managementVideoList- Grid/list view of videos with filtering and paginationVideoDetails- Detailed video page with comments and related content
Dumb Components (Presentational Components):
Header- Navigation bar with logo and user controlsFooter- Site footer with links and copyrightPlayerControls- Playback controls (play/pause, volume, fullscreen)VideoCard- Individual video display in grid/listThumbnail- Video thumbnail image with hover effectsVideoTitle- Video title text displayVideoDuration- Duration indicator with time formatSearchBar- Search input field with suggestionsVideoMetadata- Video information display (title, description, etc.)CommentsSection- Comments display with inputRelatedVideos- List of related videos
TypeScript Props Contracts
// VideoPlayer Component
interface VideoPlayerProps {
videoId: string;
initialPlaybackRate?: number;
onPlay?: () => void;
onPause?: () => void;
onEnded?: () => void;
onError?: (error: Error) => void;
}
// VideoCard Component
interface VideoCardProps {
video: VideoItem;
onClick?: (videoId: string) => void;
showDuration?: boolean;
showDescription?: boolean;
}
// VideoList Component
interface VideoListProps {
videos: VideoItem[];
loading?: boolean;
error?: string | null;
onVideoSelect: (videoId: string) => void;
onScrollEnd?: () => void;
viewMode?: 'grid' | 'list';
}
// VideoDetails Component
interface VideoDetailsProps {
videoId: string;
onBack?: () => void;
onRelatedVideoSelect?: (videoId: string) => void;
}
// SearchBar Component
interface SearchBarProps {
onSearch: (query: string) => void;
placeholder?: string;
initialValue?: string;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Playing : play()
Idle --> Paused : pause()
Playing --> Paused : pause()
Paused --> Playing : play()
Playing --> Ended : onEnded()
Paused --> Ended : onEnded()
Playing --> Error : onError()
Paused --> Error : onError()
Error --> Idle : reset()
[*] --> Loading
Loading --> Playing : onLoaded()
Loading --> Error : onError()
Component Responsibilities
VideoStreamingApp:
- Manages global routing and navigation
- Handles authentication state and protected routes
- Provides global context for theme, language, and preferences
VideoPlayer:
- Manages video playback state (play/pause/seek)
- Handles quality selection and adaptive streaming
- Manages fullscreen and keyboard controls
- Implements playback rate control
VideoList:
- Fetches and displays video collections
- Handles pagination and infinite scroll
- Manages search and filtering functionality
- Implements responsive grid/list view
VideoCard:
- Displays individual video metadata
- Handles click-to-play navigation
- Shows thumbnail and duration information
VideoDetails:
- Displays detailed video information
- Manages comments section and user interactions
- Shows related videos in a carousel or list
Reusability Decisions
-
High Reusability Components:
VideoCard- Can be reused in search results, recommendations, and playlistsSearchBar- Reusable across different content typesPlayerControls- Can be used in various player contexts
-
Feature-Specific Components:
VideoPlayer- Specific to video playback experienceCommentsSection- Video-specific interaction pattern
Component Lifecycle Considerations
-
Mounting:
- Initialize video player with proper media source
- Fetch initial video data for lists and details
- Set up keyboard event listeners
-
Updating:
- Handle video ID changes in VideoDetails
- Update playback position when seeking
- Refresh data on search query changes
-
Unmounting:
- Clean up media player resources
- Cancel ongoing API requests
- Remove event listeners and timers
Performance Considerations
-
Virtualization:
- Video lists should use virtual scrolling for large collections
- Implement proper item size measurement
-
Lazy Loading:
- Video thumbnails should be lazy-loaded as they come into view
- Video content should only load when user initiates playback
-
Memory Management:
- Clean up media resources on component unmount
- Implement proper cleanup for event listeners and timers
Accessibility Considerations
-
Keyboard Navigation:
- All interactive elements must be keyboard accessible
- Video player controls should support full keyboard navigation
-
Screen Reader Support:
- Proper ARIA labels for all interactive elements
- Video metadata should be announced to screen readers
-
Focus Management:
- Proper focus trapping in modals and player controls
- Focus restoration after interactions
3. State Management Design
Local state vs. global state decisions
For a video streaming UI, we need to carefully balance local and global state:
- Local state: Player controls (play/pause, volume, progress), UI visibility toggles (sidebar, player controls)
- Global state: User preferences (quality settings, autoplay), current video context, watch history, search queries, and user session data
State management approach
Zustand is selected as the primary state management solution for this feature because:
- Simplicity: Minimal boilerplate compared to Redux Toolkit
- TypeScript native: Excellent type inference and integration
- Scalability: Can be extended to use middleware for persistence or logging
- Performance: No unnecessary re-renders from context providers
State shape and normalization strategy
// Normalized state structure for video streaming
interface VideoState {
currentVideoId: string | null;
videos: Record<string, Video>;
playlists: Record<string, Playlist>;
userPreferences: UserPreferences;
watchHistory: WatchHistoryItem[];
searchResults: SearchState;
player: PlayerState;
}
interface Video {
id: string;
title: string;
description: string;
duration: number; // in seconds
thumbnailUrl: string;
videoUrl: string;
createdAt: Date;
tags: string[];
views: number;
likes: number;
comments: Comment[];
}
interface PlayerState {
isPlaying: boolean;
currentTime: number; // in seconds
duration: number; // in seconds
volume: number; // 0-1
playbackRate: number;
isFullscreen: boolean;
quality: string; // "1080p", "720p", etc.
isMuted: boolean;
}
interface UserPreferences {
autoPlay: boolean;
quality: string;
language: string;
subtitlesEnabled: boolean;
subtitleLanguage: string;
darkMode: boolean;
}
interface SearchState {
query: string;
results: Video[];
isLoading: boolean;
error: string | null;
}
Server state vs. client state separation
Server state includes:
- Video metadata (titles, descriptions, thumbnails)
- User watch history and preferences
- Playlist contents and metadata
Client state includes:
- Player UI state (play/pause, volume)
- Current video progress
- Search input and results cache
- UI state (sidebar visibility, modal states)
Cache invalidation strategy
- Time-based expiration: Search results expire after 5 minutes
- Event-driven invalidation: When user watches a video, invalidate related recommendations
- Manual refresh: Users can manually refresh search or playlist content
Optimistic updates design
For user interactions like:
- Adding to watchlist
- Updating preferences
- Marking as watched
We implement optimistic updates:
- Immediately update local state with the change
- Dispatch to backend API
- If API fails, rollback to previous state with error toast
Critical-path hook implementation
// VideoPlayerStore.ts - Core state management for video player
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface VideoPlayerState {
// Core player state
currentVideoId: string | null;
isPlaying: boolean;
currentTime: number;
duration: number;
volume: number;
isMuted: boolean;
playbackRate: number;
quality: string;
isFullscreen: boolean;
// User preferences
userPreferences: {
autoPlay: boolean;
quality: string;
language: string;
subtitlesEnabled: boolean;
darkMode: boolean;
};
// UI state
showControls: boolean;
showSidebar: boolean;
isModalOpen: boolean;
// Actions
setCurrentVideo: (videoId: string | null) => void;
togglePlay: () => void;
setCurrentTime: (time: number) => void;
setVolume: (volume: number) => void;
toggleMute: () => void;
setPlaybackRate: (rate: number) => void;
setQuality: (quality: string) => void;
toggleFullscreen: () => void;
updatePreferences: (preferences: Partial<UserPreferences>) => void;
setShowControls: (show: boolean) => void;
toggleSidebar: () => void;
openModal: () => void;
closeModal: () => void;
// Side effects
loadVideo: (videoId: string) => Promise<void>;
saveProgress: (videoId: string, progress: number) => void;
}
const useVideoPlayerStore = create<VideoPlayerState>()(
persist(
(set, get) => ({
// Initial state
currentVideoId: null,
isPlaying: false,
currentTime: 0,
duration: 0,
volume: 1,
isMuted: false,
playbackRate: 1,
quality: '720p',
isFullscreen: false,
userPreferences: {
autoPlay: true,
quality: '720p',
language: 'en',
subtitlesEnabled: false,
darkMode: false,
},
showControls: true,
showSidebar: true,
isModalOpen: false,
// Actions
setCurrentVideo: (videoId) => set({ currentVideoId: videoId }),
togglePlay: () => set((state) => ({ isPlaying: !state.isPlaying })),
setCurrentTime: (time) => set({ currentTime: time }),
setVolume: (volume) => set({ volume, isMuted: volume === 0 }),
toggleMute: () => set((state) => ({ isMuted: !state.isMuted })),
setPlaybackRate: (rate) => set({ playbackRate: rate }),
setQuality: (quality) => {
set({ quality });
// Update user preferences
set((state) => ({
userPreferences: {
...state.userPreferences,
quality,
},
}));
},
toggleFullscreen: () => set((state) => ({ isFullscreen: !state.isFullscreen })),
updatePreferences: (preferences) => {
set((state) => ({
userPreferences: {
...state.userPreferences,
...preferences,
},
}));
},
setShowControls: (show) => set({ showControls: show }),
toggleSidebar: () => set((state) => ({ showSidebar: !state.showSidebar })),
openModal: () => set({ isModalOpen: true }),
closeModal: () => set({ isModalOpen: false }),
// Side effects
loadVideo: async (videoId) => {
set({ currentVideoId: videoId, isPlaying: true });
// Simulate API call to fetch video metadata
try {
// In real implementation, this would be:
// const video = await fetchVideoMetadata(videoId);
// set({ duration: video.duration });
// For demo purposes, we'll simulate loading
setTimeout(() => {
set({ duration: 180 }); // 3 minutes mock duration
}, 100);
} catch (error) {
console.error('Failed to load video', error);
set({ currentVideoId: null });
}
},
saveProgress: (videoId, progress) => {
// In a real app, this would persist to backend
console.log(`Saving progress for ${videoId}: ${progress}s`);
},
}),
{
name: 'video-player-storage',
partialize: (state) => ({
userPreferences: state.userPreferences,
quality: state.quality,
}),
}
)
);
// Custom hook for video player with side effects
export const useVideoPlayer = () => {
const {
currentVideoId,
isPlaying,
currentTime,
duration,
volume,
isMuted,
playbackRate,
quality,
isFullscreen,
userPreferences,
showControls,
showSidebar,
isModalOpen,
setCurrentVideo,
togglePlay,
setCurrentTime,
setVolume,
toggleMute,
setPlaybackRate,
setQuality,
toggleFullscreen,
updatePreferences,
setShowControls,
toggleSidebar,
openModal,
closeModal,
loadVideo,
saveProgress,
} = useVideoPlayerStore();
// Handle progress saving
React.useEffect(() => {
if (currentVideoId && currentTime > 0 && currentTime < duration) {
const saveTimer = setTimeout(() => {
saveProgress(currentVideoId, currentTime);
}, 5000); // Save every 5 seconds
return () => clearTimeout(saveTimer);
}
}, [currentTime, currentVideoId, duration]);
// Handle fullscreen changes
React.useEffect(() => {
const handleFullscreenChange = () => {
set({ isFullscreen: !!document.fullscreenElement });
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
return {
// State
currentVideoId,
isPlaying,
currentTime,
duration,
volume,
isMuted,
playbackRate,
quality,
isFullscreen,
userPreferences,
showControls,
showSidebar,
isModalOpen,
// Actions
setCurrentVideo,
togglePlay,
setCurrentTime,
setVolume,
toggleMute,
setPlaybackRate,
setQuality,
toggleFullscreen,
updatePreferences,
setShowControls,
toggleSidebar,
openModal,
closeModal,
// Side effects
loadVideo,
saveProgress,
};
};
// Example usage in component:
/*
const VideoPlayerComponent = () => {
const {
currentVideoId,
isPlaying,
currentTime,
duration,
volume,
setVolume,
togglePlay,
setCurrentTime,
} = useVideoPlayer();
return (
<div className="video-player">
{currentVideoId && (
<video
autoPlay={isPlaying}
onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}
onPlay={() => togglePlay()}
onPause={() => togglePlay()}
>
<source src={getVideoUrl(currentVideoId)} type="video/mp4" />
</video>
)}
<div className="player-controls">
<button onClick={togglePlay}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<input
type="range"
min="0"
max={duration}
value={currentTime}
onChange={(e) => setCurrentTime(parseFloat(e.target.value))}
/>
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={(e) => setVolume(parseFloat(e.target.value))}
/>
</div>
</div>
);
};
*/
The hook provides:
- Core player state management with proper TypeScript typing
- Side effect handling for progress saving and fullscreen changes
- Persistence through localStorage with selective state persistence
- Optimistic updates pattern for user preference changes
- Clean separation of concerns between UI state and business logic
4. Routing & Navigation
Route Structure and Hierarchy
graph TD
A[Root] --> B[Public Routes]
A --> C[Protected Routes]
B --> D[Home]
B --> E[Search]
B --> F[Categories]
C --> G[Watch]
C --> H[Profile]
C --> I[Library]
C --> J[Settings]
G --> K[Video Player]
The video streaming application will have a clear route hierarchy:
- Public Routes: Home, Search, Categories - accessible without authentication
- Protected Routes: Watch (video player), Profile, Library, Settings - require authentication
- Nested Structure: Video player will be a nested route under Watch with video ID parameter
Code Splitting Strategy
Route-based code splitting will be implemented using React.lazy and Suspense:
// routes.tsx
const Home = lazy(() => import('pages/Home'));
const Search = lazy(() => import('pages/Search'));
const VideoPlayer = lazy(() => import('pages/VideoPlayer'));
const Profile = lazy(() => import('pages/Profile'));
// Route configuration with dynamic imports
export const routes = [
{
path: '/',
element: <Home />,
index: true,
},
{
path: '/search',
element: <Search />,
},
{
path: '/watch/:videoId',
element: <VideoPlayer />,
},
{
path: '/profile',
element: <Profile />,
requiresAuth: true,
},
];
Justification: Route-based splitting provides optimal bundle loading for user journeys. The video player is the heaviest component and should be loaded only when needed, while other pages can be preloaded.
Navigation Guards and Authentication Flows
// auth-guard.tsx
const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return children;
};
// ProtectedRoute component
const ProtectedRoute: React.FC<{
element: React.ReactNode;
path: string
}> = ({ element, path }) => (
<Route
path={path}
element={
<AuthGuard>
{element}
</AuthGuard>
}
/>
);
Authentication flow:
- User accesses protected route without valid session
- Redirect to login page with return URL in query parameter
- After successful login, redirect user back to original route
Deep Linking Support and URL Parameter Design
URL structure will support deep linking with clear parameter semantics:
// URL patterns for video streaming
// /watch/:videoId?autoplay=true&time=120
// /search?q=movie&category=action&page=2
// /library?filter=watched&sort=date
// Route parameters and query string handling
interface VideoPlayerParams {
videoId: string;
}
interface SearchQueryParams {
q?: string;
category?: string;
page?: string;
}
Browser History Management and Scroll Restoration
// scroll-restoration.tsx
const ScrollRestoration: React.FC = () => {
const location = useLocation();
useEffect(() => {
// Restore scroll position when navigating back to same route
const savedScrollPosition = sessionStorage.getItem(`scroll-position-${location.pathname}`);
if (savedScrollPosition) {
window.scrollTo(0, parseInt(savedScrollPosition));
} else {
window.scrollTo(0, 0);
}
// Save scroll position when leaving route
const handleScroll = () => {
sessionStorage.setItem(`scroll-position-${location.pathname}`, window.scrollY.toString());
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [location.pathname]);
return null;
};
// App.tsx integration
<BrowserRouter>
<ScrollRestoration />
<Routes>
{/* ... routes */}
</Routes>
</BrowserRouter>
Scroll restoration strategy:
- Uses sessionStorage to persist scroll positions for each route
- Handles browser back/forward navigation gracefully
- Maintains user context when navigating between similar content pages
5. API Integration Layer
API Client Design Pattern
We'll implement a centralized API client using Axios with a custom wrapper to handle authentication, request/response transformation, and error handling. The client will be built as a singleton pattern to ensure consistent usage across the application.
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
class ApiClient {
private client: AxiosInstance;
constructor(baseURL: string) {
this.client = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor for auth token injection
this.client.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling and normalization
this.client.interceptors.response.use(
(response) => response,
(error) => {
// Handle authentication errors
if (error.response?.status === 401) {
// Clear auth token and redirect to login
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}
public get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.client.get<T>(url, config);
}
public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.client.post<T>(url, data, config);
}
public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.client.put<T>(url, data, config);
}
public delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.client.delete<T>(url, config);
}
}
export const apiClient = new ApiClient(process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com');
Request/Response Interceptors
The request interceptor handles authentication token injection, while the response interceptor manages error states and provides normalized error responses. Authentication tokens are stored in localStorage for persistence across sessions.
Error Handling and Retry Strategy
We implement a retry mechanism with exponential backoff for transient network errors (408, 429, 502, 503, 504) with a maximum of 3 retries:
// api/retry.ts
export const retryWithBackoff = async <T>(
fn: () => Promise<T>,
retries = 3,
delay = 1000
): Promise<T> => {
try {
return await fn();
} catch (error) {
if (retries > 0 && [408, 429, 502, 503, 504].includes(error.response?.status)) {
await new Promise((resolve) => setTimeout(resolve, delay));
return retryWithBackoff(fn, retries - 1, delay * 2);
}
throw error;
}
};
Loading, Error, and Empty States Design
We implement a consistent state management pattern across all data fetching components:
// types/api.ts
export type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
export type VideoResponse = {
id: string;
title: string;
description: string;
thumbnailUrl: string;
duration: number;
views: number;
uploadDate: string;
category: string;
isFeatured?: boolean;
};
export type VideoListResponse = {
videos: VideoResponse[];
total: number;
page: number;
limit: number;
};
Data Fetching Strategy
For video streaming, we use a hybrid approach:
- Home page: Client-side rendering with preloaded featured content (SSR)
- Video detail pages: Server-side rendering for SEO and performance
- Search results: Client-side rendering with infinite scroll
- User library: Client-side caching with optimistic updates
API Response Transformation Layer
We create a mapping layer to transform raw API responses into domain models:
// api/mappers.ts
import { VideoResponse, VideoListResponse } from './types';
export const mapVideoResponse = (response: VideoResponse): Video => ({
id: response.id,
title: response.title,
description: response.description,
thumbnailUrl: response.thumbnailUrl,
duration: response.duration,
views: response.views,
uploadDate: new Date(response.uploadDate),
category: response.category,
isFeatured: response.isFeatured || false,
});
export const mapVideoListResponse = (response: VideoListResponse): VideoList => ({
videos: response.videos.map(mapVideoResponse),
total: response.total,
page: response.page,
limit: response.limit,
});
Sequence Diagram for Primary Data Flows
sequenceDiagram
participant U as User
participant C as Component
participant A as ApiClient
participant S as Server
U->>C: Request video list
C->>A: Fetch videos with params
A->>S: GET /videos?page=1&limit=20
S-->>A: Response with videos
A-->>C: Transformed video data
C->>U: Render video list with thumbnails and metadata
The sequence diagram shows the typical flow for fetching and displaying video content, including the transformation of raw API responses into component-ready data structures. The flow demonstrates proper error handling and state management throughout the data fetching lifecycle.
For video streaming specifically, we also implement:
- Progressive loading of video metadata
- Chunked content delivery for large video files
- Adaptive bitrate selection based on network conditions
- Cache headers and CDN integration for optimal content delivery
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for 95% of users
- First Input Delay (FID): <100ms for 90% of users
- Cumulative Layout Shift (CLS): <0.1 for 95% of users
Bundle Size Budget per Route
- Home Page: <500KB (including all dependencies)
- Video Player Page: <300KB (excluding video assets)
- Search Results: <250KB
- User Profile: <150KB
Critical Rendering Path Optimization
// Critical CSS extraction and inlining for key routes
const criticalCSS = `
.video-player-container {
display: flex;
min-height: 100vh;
}
.video-player {
flex: 1;
max-width: 100%;
}
.video-controls {
position: absolute;
bottom: 0;
width: 100%;
background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
}
`;
// Preload critical resources
const preloadCriticalResources = () => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = '/critical.css';
document.head.appendChild(link);
};
Image Optimization Strategy
- Format: WebP for modern browsers, JPEG fallback
- Lazy Loading:
loading="lazy"with Intersection Observer polyfill - Responsive Images:
srcsetwith multiple sizes (320px, 480px, 768px, 1024px) - Image Compression: 85% quality for thumbnails, 90% for player
Font Loading Strategy
// Font loading with font-display: swap and preloading
const preloadFonts = () => {
const fontLinks = [
{ href: '/fonts/inter-regular.woff2', weight: '400' },
{ href: '/fonts/inter-semibold.woff2', weight: '600' }
];
fontLinks.forEach(font => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'font';
link.crossOrigin = 'anonymous';
link.href = font.href;
link.type = 'font/woff2';
document.head.appendChild(link);
});
};
Memoization Strategy
// VideoCard component with proper memoization
const VideoCard = React.memo(({ video, onClick }: VideoCardProps) => {
const thumbnailUrl = useMemo(() =>
generateThumbnailUrl(video.id, video.thumbnailWidth),
[video.id, video.thumbnailWidth]
);
const formattedDuration = useMemo(() =>
formatDuration(video.duration),
[video.duration]
);
return (
<div className="video-card" onClick={() => onClick(video)}>
<img
src={thumbnailUrl}
alt={video.title}
loading="lazy"
width={video.thumbnailWidth}
height={video.thumbnailHeight}
/>
<div className="video-info">
<h3>{video.title}</h3>
<p>{formattedDuration}</p>
</div>
</div>
);
});
// VideoPlayer component with useCallback for event handlers
const VideoPlayer = ({ video }: VideoPlayerProps) => {
const [currentTime, setCurrentTime] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const handleTimeUpdate = useCallback((time: number) => {
setCurrentTime(time);
}, []);
const handlePlayPause = useCallback(() => {
setIsPlaying(!isPlaying);
}, [isPlaying]);
return (
<div className="video-player">
<video
onTimeUpdate={handleTimeUpdate}
onPlayPause={handlePlayPause}
>
<source src={video.url} type="video/mp4" />
</video>
</div>
);
};
Virtualization for Large Lists
// VideoList component with react-window for large lists
import { FixedSizeList as List } from 'react-window';
const VideoList = ({ videos, onVideoSelect }: VideoListProps) => {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<VideoCard
video={videos[index]}
onClick={onVideoSelect}
/>
</div>
);
return (
<List
height={600}
itemCount={videos.length}
itemSize={150}
width="100%"
>
{Row}
</List>
);
};
Prefetching and Preloading Strategy
// Video preloading strategy for next video in playlist
const useVideoPreloader = (videoId: string) => {
const [isPreloaded, setIsPreloaded] = useState(false);
useEffect(() => {
// Preload next video in playlist
const preloadNextVideo = async () => {
try {
const nextVideo = await fetchNextVideo(videoId);
const videoElement = new Video();
videoElement.preload = 'metadata';
videoElement.src = nextVideo.url;
setIsPreloaded(true);
} catch (error) {
console.error('Failed to preload video:', error);
}
};
preloadNextVideo();
}, [videoId]);
return isPreloaded;
};
// Prefetch popular videos for offline access
const prefetchPopularVideos = async () => {
const popularVideos = await fetchPopularVideos();
// Prefetch thumbnails
popularVideos.slice(0, 10).forEach(video => {
const img = new Image();
img.src = generateThumbnailUrl(video.id, 320);
});
};
7. Error Handling & Resilience
Error Boundary Placement Strategy
graph TD
A[App Root] --> B[ErrorBoundary]
B --> C[VideoPlayer]
B --> D[VideoList]
B --> E[VideoDetails]
B --> F[UserMenu]
C --> G[PlayerErrorBoundary]
D --> H[VideoListErrorBoundary]
E --> I[VideoDetailsErrorBoundary]
Global Error Handler Design
// src/core/error/GlobalErrorHandler.tsx
import React from 'react';
interface GlobalErrorState {
hasError: boolean;
error: Error | null;
errorInfo: React.ErrorInfo | null;
}
class GlobalErrorHandler extends React.Component<{}, GlobalErrorState> {
constructor(props: {}) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error: Error): GlobalErrorState {
return { hasError: true, error, errorInfo: null };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log error to monitoring service
console.error('Global error caught:', error, errorInfo);
// Report to external error tracking service (e.g., Sentry)
if (window.Sentry) {
window.Sentry.captureException(error, {
contexts: { react: { errorInfo } }
});
}
this.setState({ errorInfo });
}
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo?.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
export default GlobalErrorHandler;
Offline / Network Failure Handling
// src/core/network/NetworkStatusProvider.tsx
import React, { useState, useEffect } from 'react';
interface NetworkStatusContextType {
isOnline: boolean;
isSlowConnection: boolean;
retryNetworkOperation: () => void;
}
const NetworkStatusContext = React.createContext<NetworkStatusContextType>({
isOnline: true,
isSlowConnection: false,
retryNetworkOperation: () => {},
});
export const NetworkStatusProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine);
const [isSlowConnection, setIsSlowConnection] = useState<boolean>(false);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const retryNetworkOperation = () => {
// Implement retry logic with exponential backoff
console.log('Retrying network operation...');
};
return (
<NetworkStatusContext.Provider value={{
isOnline,
isSlowConnection,
retryNetworkOperation
}}>
{children}
</NetworkStatusContext.Provider>
);
};
export const useNetworkStatus = () => {
const context = React.useContext(NetworkStatusContext);
if (!context) {
throw new Error('useNetworkStatus must be used within NetworkStatusProvider');
}
return context;
};
Graceful Degradation Patterns
// src/components/VideoPlayer/VideoPlayer.tsx
import React, { useState, useEffect } from 'react';
import { useNetworkStatus } from '../../core/network/NetworkStatusProvider';
interface VideoPlayerProps {
videoId: string;
onError?: (error: Error) => void;
}
const VideoPlayer: React.FC<VideoPlayerProps> = ({ videoId, onError }) => {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [isBuffering, setIsBuffering] = useState(false);
const { isOnline } = useNetworkStatus();
useEffect(() => {
const handleVideoError = (e: Event) => {
const videoError = new Error('Video playback failed');
setError(videoError);
onError?.(videoError);
};
const handleVideoLoadStart = () => {
setIsLoading(true);
};
const handleVideoLoadedData = () => {
setIsLoading(false);
};
const handleVideoWaiting = () => {
setIsBuffering(true);
};
const handleVideoPlaying = () => {
setIsBuffering(false);
};
const videoElement = document.getElementById(`video-${videoId}`) as HTMLVideoElement;
if (videoElement) {
videoElement.addEventListener('error', handleVideoError);
videoElement.addEventListener('loadstart', handleVideoLoadStart);
videoElement.addEventListener('loadeddata', handleVideoLoadedData);
videoElement.addEventListener('waiting', handleVideoWaiting);
videoElement.addEventListener('playing', handleVideoPlaying);
}
return () => {
if (videoElement) {
videoElement.removeEventListener('error', handleVideoError);
videoElement.removeEventListener('loadstart', handleVideoLoadStart);
videoElement.removeEventListener('loadeddata', handleVideoLoadedData);
videoElement.removeEventListener('waiting', handleVideoWaiting);
videoElement.removeEventListener('playing', handleVideoPlaying);
}
};
}, [videoId, onError]);
const renderFallbackUI = () => {
if (!isOnline) {
return (
<div className="offline-fallback">
<p>You are offline. Please check your connection.</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
if (error) {
return (
<div className="error-fallback">
<p>Failed to load video. Please try again.</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return (
<div className="loading-fallback">
<p>Loading video...</p>
</div>
);
};
if (isLoading || isBuffering) {
return (
<div className="video-player-loading">
<div className="spinner"></div>
</div>
);
}
if (error || !isOnline) {
return renderFallbackUI();
}
return (
<div className="video-player-container">
<video
id={`video-${videoId}`}
controls
className="video-player"
poster="/placeholder-thumbnail.jpg"
>
<source src={`/api/videos/${videoId}/stream`} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
);
};
export default VideoPlayer;
User Feedback on Errors
// src/components/Toast/ToastManager.tsx
import React, { useState, useEffect } from 'react';
interface Toast {
id: string;
message: string;
type: 'error' | 'warning' | 'success' | 'info';
duration?: number;
}
const ToastManager: React.FC = () => {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = (toast: Omit<Toast, 'id'>) => {
const id = Date.now().toString();
setToasts(prev => [...prev, { ...toast, id }]);
if (toast.duration !== 0) {
setTimeout(() => removeToast(id), toast.duration || 5000);
}
};
const removeToast = (id: string) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
return (
<div className="toast-container">
{toasts.map(toast => (
<div
key={toast.id}
className={`toast toast--${toast.type}`}
role="alert"
aria-live="polite"
>
<span className="toast-message">{toast.message}</span>
<button
className="toast-close"
onClick={() => removeToast(toast.id)}
aria-label="Close toast"
>
×
</button>
</div>
))}
</div>
);
};
export default ToastManager;
// src/hooks/useToast.ts
import { useCallback } from 'react';
import { addToast } from '../components/Toast/ToastManager';
export const useToast = () => {
const showToast = useCallback((message: string, type: 'error' | 'warning' | 'success' | 'info', duration?: number) => {
addToast({ message, type, duration });
}, []);
return { showToast };
};
Error Boundary Components
// src/core/error/VideoListErrorBoundary.tsx
import React from 'react';
interface VideoListErrorState {
hasError: boolean;
error: Error | null;
}
class VideoListErrorBoundary extends React.Component<{ children: React.ReactNode }, VideoListErrorState> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): VideoListErrorState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('VideoList component error:', error, errorInfo);
// Report to monitoring service
if (window.Sentry) {
window.Sentry.captureException(error, {
contexts: { react: { errorInfo } }
});
}
}
render() {
if (this.state.hasState) {
return (
<div className="error-boundary">
<h3>Failed to load video list</h3>
<p>Please try again later or contact support.</p>
<button onClick={() => window.location.reload()}>
Reload
</button>
</div>
);
}
return this.props.children;
}
}
export default VideoListErrorBoundary;
Retry Logic Implementation
// src/core/network/RetryableRequest.ts
import { useState, useEffect } from 'react';
interface RetryConfig {
maxRetries?: number;
retryDelay?: number;
backoffMultiplier?: number;
}
export const useRetryableRequest = <T>(
requestFn: () => Promise<T>,
config: RetryConfig = {}
): {
data: T | null;
loading: boolean;
error: Error | null;
retry: () => void;
} => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
const [retryCount, setRetryCount] = useState<number>(0);
const {
maxRetries = 3,
retryDelay = 1000,
backoffMultiplier = 2
} = config;
const executeRequest = async () => {
setLoading(true);
setError(null);
try {
const result = await requestFn();
setData(result);
setRetryCount(0);
} catch (err) {
if (retryCount < maxRetries) {
const delay = retryDelay * Math.pow(backoffMultiplier, retryCount);
setTimeout(() => {
setRetryCount(prev => prev + 1);
}, delay);
} else {
setError(err instanceof Error ? err : new Error('Request failed'));
}
} finally {
setLoading(false);
}
};
useEffect(() => {
executeRequest();
}, [retryCount]);
const retry = () => {
setRetryCount(0);
};
return { data, loading, error, retry };
};
8. Testing Strategy
Unit Testing Approach
Component Tests (80% coverage target):
- Core UI components: VideoPlayer, VideoCard, Playlist, PlayerControls
- Utility functions: time formatting, playback rate calculation, buffer state management
- Custom hooks: useVideoPlayer, usePlaylist, usePlaybackState
Testing Framework: Jest + React Testing Library with TypeScript support
// Example: VideoCard component test
import { render, screen } from '@testing-library/react';
import { VideoCard } from './VideoCard';
describe('VideoCard', () => {
const mockVideo = {
id: '123',
title: 'Sample Video',
duration: '12:34',
thumbnailUrl: '/thumbnail.jpg',
views: '1.2M',
uploadDate: '2023-01-01'
};
it('renders video title and thumbnail', () => {
render(<VideoCard video={mockVideo} />);
expect(screen.getByText('Sample Video')).toBeInTheDocument();
expect(screen.getByAltText('Sample Video')).toHaveAttribute('src', '/thumbnail.jpg');
});
it('displays view count and upload date', () => {
render(<VideoCard video={mockVideo} />);
expect(screen.getByText('1.2M views')).toBeInTheDocument();
expect(screen.getByText('2023-01-01')).toBeInTheDocument();
});
});
Utility Function Tests:
// timeFormatter.test.ts
import { formatTime } from './timeFormatter';
describe('formatTime', () => {
it('formats seconds to MM:SS', () => {
expect(formatTime(65)).toBe('01:05');
});
it('handles zero seconds', () => {
expect(formatTime(0)).toBe('00:00');
});
});
Integration Testing Strategy
API Integration Tests:
- End-to-end flow from video selection to playback
- Playlist loading and navigation between videos
- Error state handling for failed API calls
Component Integration Tests:
// VideoPlayer.integration.test.ts
import { render, screen } from '@testing-library/react';
import { VideoPlayer } from './VideoPlayer';
describe('VideoPlayer Integration', () => {
it('loads video and displays controls', async () => {
const mockVideo = { id: '123', url: '/video.mp4' };
render(<VideoPlayer video={mockVideo} />);
expect(screen.getByTestId('video-player')).toBeInTheDocument();
expect(screen.getByTestId('playback-controls')).toBeInTheDocument();
});
});
E2E Testing Scope
Testing Tools: Playwright (cross-browser support, reliable DOM interaction)
Critical User Flows:
- Video search and filtering
- Video playback with controls (play/pause, volume, fullscreen)
- Playlist navigation and autoplay
- Responsive behavior on mobile/desktop
- Error recovery from network failures
E2E Test Coverage:
- Desktop: Chrome, Firefox, Safari
- Mobile: iOS Safari, Android Chrome
- Tablet: Responsive breakpoints
Visual Regression Testing
Tool: Percy (or similar visual testing service)
Test Coverage:
- Video player in various states (playing, paused, buffering)
- Playlist view with different content lengths
- Responsive layouts across device sizes
- Dark/light mode variants
Accessibility Testing Automation
Framework: axe-core + testing-library/axe
Test Coverage:
// accessibility.test.ts
import { axe, toHaveNoViolations } from 'axe-core';
import { render } from '@testing-library/react';
expect.extend(toHaveNoViolations);
it('maintains accessibility standards', async () => {
const { container } = render(<VideoPlayer video={mockVideo} />);
const results = await axe.run(container);
expect(results).toHaveNoViolations();
});
Test Data Strategy
Test Data Generation:
- Mock data factories for videos, playlists, and user preferences
- Fallback test data for edge cases (empty playlists, malformed responses)
- Realistic but controlled datasets for performance testing
Fixtures:
// fixtures/video.ts
export const mockVideo = {
id: '123',
title: 'Sample Video',
description: 'A sample video for testing',
duration: 3600,
thumbnailUrl: '/thumbnail.jpg',
views: 1234567,
uploadDate: new Date('2023-01-01'),
isLive: false,
playbackUrl: '/video.mp4'
};
export const mockPlaylist = [
mockVideo,
{ ...mockVideo, id: '456', title: 'Another Video' }
];
Test Environment Configuration
Isolation Strategy:
- Jest test environment with jsdom for DOM simulation
- Mocked HTTP requests using MSW (Mock Service Worker)
- Isolated test databases for stateful components
Performance Testing:
- Large playlist rendering (100+ items)
- Buffer state transitions under various network conditions
- Memory leak detection in long-running sessions
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The video streaming UI must meet WCAG 2.1 AA standards with the following key requirements:
- Text Contrast: All text must have a contrast ratio of at least 4.5:1 against its background
- Keyboard Navigation: Full keyboard accessibility for all interactive elements (play/pause, volume, fullscreen)
- Focus Management: Clear focus indicators and proper focus order through all interactive components
- Screen Reader Support: Complete ARIA labeling for video player controls and metadata
- Captions & Transcripts: Support for closed captions and audio descriptions (with toggle controls)
- Sufficient Time: Video playback controls must not auto-play without user interaction, and users must have sufficient time to consume content
- No Keyboard Traps: Focus can be moved through all interactive elements and returned to the previous focus point
Keyboard navigation support
All video player controls must be fully operable via keyboard:
// Keyboard navigation handler for video player controls
const handleKeyboardNavigation = (event: KeyboardEvent) => {
const { key } = event;
switch (key) {
case ' ':
// Toggle play/pause
event.preventDefault();
togglePlayPause();
break;
case 'ArrowLeft':
// Seek backward 10 seconds
event.preventDefault();
seekBackward(10);
break;
case 'ArrowRight':
// Seek forward 10 seconds
event(event.preventDefault());
seekForward(10);
break;
case 'ArrowUp':
// Increase volume
event.preventDefault();
adjustVolume(0.1);
break;
case 'ArrowDown':
// Decrease volume
event.preventDefault();
adjustVolume(-0.1);
break;
case 'f':
// Toggle fullscreen
event.preventDefault();
toggleFullscreen();
break;
case 'c':
// Toggle captions
event.preventDefault();
toggleCaptions();
break;
case 'm':
// Toggle mute
event.preventDefault();
toggleMute();
break;
default:
break;
}
};
Focus management strategy
Focus trapping within the video player modal and proper focus restoration:
// Focus management for video player modal
const useVideoPlayerFocus = (isOpen: boolean) => {
const playerRef = useRef<HTMLDivElement>(null);
const previousActiveElement = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
previousActiveElement.current = document.activeElement as HTMLElement;
const playerElement = playerRef.current;
if (playerElement) {
// Trap focus within player controls
const firstFocusable = playerElement.querySelector(
'button, [tabindex]:not([tabindex="-1"])'
) as HTMLElement;
if (firstFocusable) {
firstFocusable.focus();
}
}
} else if (previousActiveElement.current) {
// Restore focus to previous element
previousActiveElement.current.focus();
}
}, [isOpen]);
return playerRef;
};
ARIA roles and attributes plan
// Video player component with proper ARIA attributes
const VideoPlayer = ({
videoUrl,
title,
duration,
isPlaying,
currentTime,
volume,
captionsEnabled
}: VideoPlayerProps) => {
const [isFullscreen, setIsFullscreen] = useState(false);
return (
<div
role="application"
aria-label={`Video player for ${title}`}
className="video-player-container"
>
<video
aria-label={`Video content: ${title}`}
role="region"
controls={false}
className="video-element"
src={videoUrl}
/>
{/* Player controls with ARIA */}
<div
role="toolbar"
aria-label="Video player controls"
className="video-controls"
>
<button
role="button"
aria-label={isPlaying ? "Pause video" : "Play video"}
aria-pressed={isPlaying}
onClick={togglePlayPause}
className="control-button"
>
{isPlaying ? "⏸" : "▶"}
</button>
<button
role="button"
aria-label={volume > 0 ? "Mute video" : "Unmute video"}
onClick={toggleMute}
className="control-button"
>
{volume > 0 ? "🔊" : "🔇"}
</button>
<div className="progress-container">
<span
role="progressbar"
aria-valuenow={currentTime}
aria-valuemin="0"
aria-valuemax={duration}
className="progress-bar"
>
{formatTime(currentTime)} / {formatTime(duration)}
</span>
</div>
<button
role="button"
aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
onClick={toggleFullscreen}
className="control-button"
>
{isFullscreen ? "⛶" : "⛶"}
</button>
</div>
</div>
);
};
Screen reader compatibility
All interactive elements must provide appropriate screen reader context:
// Enhanced accessibility for video list items
const VideoListItem = ({
title,
duration,
thumbnailUrl,
isPlaying,
onClick
}: VideoListItemProps) => {
return (
<div
role="article"
aria-labelledby={`video-title-${title}`}
className="video-list-item"
>
<img
src={thumbnailUrl}
alt={`Thumbnail for ${title}`}
className="video-thumbnail"
loading="lazy"
/>
<div className="video-info">
<h3
id={`video-title-${title}`}
className="video-title"
>
{title}
</h3>
<span
role="status"
aria-live="polite"
className="video-duration"
>
{duration}
</span>
<button
role="button"
aria-label={`Play ${title}`}
onClick={onClick}
className="play-button"
>
▶ Play
</button>
</div>
</div>
);
};
Keyboard focus indicators and visual feedback
/* Focus styles for video player controls */
.video-controls button:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
/* Ensure focus is visible on all interactive elements */
.video-controls button:focus-visible {
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
/* Skip link for keyboard users */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: white;
padding: 8px 16px;
text-decoration: none;
transition: top 0.3s ease;
}
.skip-link:focus {
top: 10px;
}
Caption and transcript support
// Caption toggle with screen reader announcement
const CaptionToggle = ({
captionsEnabled,
onToggle
}: CaptionToggleProps) => {
const [announcement, setAnnouncement] = useState<string | null>(null);
const handleToggle = () => {
const newStatus = !captionsEnabled;
onToggle(newStatus);
setAnnouncement(
newStatus
? "Captions enabled"
: "Captions disabled"
);
// Clear announcement after 3 seconds
setTimeout(() => setAnnouncement(null), 3000);
};
return (
<>
<button
role="switch"
aria-checked={captionsEnabled}
aria-label={`Toggle captions ${captionsEnabled ? "off" : "on"}`}
onClick={handleToggle}
className="caption-toggle"
>
CC
</button>
<span
role="status"
aria-live="polite"
className="sr-only"
>
{announcement}
</span>
</>
);
};
Keyboard navigation flow
- Tab Navigation: Users can navigate through all controls using the Tab key
- Spacebar: Toggles play/pause state
- Arrow Keys: Seek forward/backward, adjust volume, navigate menus
- Enter Key: Activates selected controls
- Escape Key: Closes modals and resets focus to previous context
Focus trapping in modal context
When video player opens as a modal, focus is trapped within the player controls:
// Focus trap implementation for video modal
const useFocusTrap = (isOpen: boolean) => {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) return;
const handleFocusTrap = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
const focusableElements = modalRef.current?.querySelectorAll(
'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
) || [];
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
if (event.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
event.preventDefault();
} else if (!event.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
event.preventDefault();
}
}
};
document.addEventListener('keydown', handleFocusTrap);
return () => document.removeEventListener('keydown', handleFocusTrap);
}, [isOpen]);
return modalRef;
};
10. Security Considerations
XSS Prevention Strategy
- Input sanitization: All user-generated content (comments, titles, descriptions) must be sanitized using a robust library like DOMPurify before rendering to prevent script injection
- Content Security Policy (CSP): Implement strict CSP headers to block inline scripts and restrict external resource loading
- Template literal escaping: All dynamic content rendered in JSX must be properly escaped to prevent XSS in React's dangerouslySetInnerHTML usage
- Video content validation: Validate all video metadata and URLs to prevent malicious content injection
CSRF Protection
- CSRF tokens: Implement anti-CSRF tokens for all state-modifying API requests (playlists, favorites, comments)
- Token storage: Store CSRF tokens in secure HTTP-only cookies or React context with proper lifecycle management
- Token validation: Server-side validation of CSRF tokens for all authenticated operations
Content Security Policy (CSP) Headers
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
media-src 'self' blob:;
connect-src 'self' wss: https:;
Sensitive Data Handling in Frontend State
- Token management: Never store authentication tokens in localStorage/sessionStorage; use secure HTTP-only cookies with SameSite flags
- Encrypted storage: For sensitive user preferences, implement encryption using Web Crypto API before storing in IndexedDB
- Memory cleanup: Ensure tokens and sensitive data are properly cleared from memory when user logs out or session expires
- State obfuscation: Avoid storing sensitive video metadata in client-side state that could be exposed via browser dev tools
Input Sanitization Approach
- API input validation: All API parameters must be validated server-side regardless of client-side sanitization
- Video URL validation: Implement strict regex patterns and domain whitelisting for video URLs to prevent malicious redirection
- User content filtering: Use a combination of regex-based filtering and third-party content moderation services for comments and titles
- File type validation: Server-side verification of video file formats to prevent malicious content upload
Additional Security Measures
- Rate limiting: Client-side request throttling to prevent abuse of video streaming endpoints
- Feature flags: Implement feature flagging for sensitive operations (e.g., user-generated content moderation)
- Secure headers: Implement X-Content-Type-Options, X-Frame-Options, and X-XSS-Protection headers
- Audit logging: Track sensitive operations in client-side logs for security monitoring (when appropriate)
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Following BEM-style naming with feature-scoped keys:
// Translation keys for video streaming UI
{
"videoPlayer": {
"play": "Play",
"pause": "Pause",
"mute": "Mute",
"unmute": "Unmute",
"fullscreen": "Fullscreen",
"exitFullscreen": "Exit Fullscreen",
"volume": "Volume",
"currentTime": "Current Time",
"duration": "Duration",
"settings": "Settings",
"quality": "Quality",
"subtitle": "Subtitle",
"captions": "Captions"
},
"videoList": {
"title": "Videos",
"loading": "Loading videos...",
"noResults": "No videos found",
"sortBy": "Sort by",
"filterBy": "Filter by"
},
"videoCard": {
"duration": "{{duration}} min",
"views": "{{views}} views",
"published": "Published {{date}}"
},
"search": {
"placeholder": "Search videos...",
"results": "Search results for '{{query}}'",
"noResults": "No videos found for '{{query}}'"
}
}
RTL Support Considerations
RTL support requires:
- Component layout adjustments (flex direction, text alignment)
- Icon rotation for directional indicators (play/pause buttons)
- Right-aligned sidebar navigation
- Proper text direction handling in form fields
Date, Number, Currency Formatting Strategy
Using Intl API for locale-aware formatting:
// Format helpers
const formatDate = (date: Date, locale: string): string => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
};
const formatDuration = (seconds: number, locale: string): string => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return new Intl.NumberFormat(locale, {
style: 'unit',
unit: 'minute',
unitDisplay: 'long'
}).format(minutes);
};
const formatViews = (views: number, locale: string): string => {
return new Intl.NumberFormat(locale, {
notation: 'compact',
compactDisplay: 'short'
}).format(views);
};
Locale Detection and Switching Strategy
Automatic detection via browser navigator.language with fallback to English:
// Locale detection and switching logic
const detectLocale = (): string => {
const storedLocale = localStorage.getItem('preferredLocale');
if (storedLocale) return storedLocale;
const browserLang = navigator.language.split('-')[0];
const supportedLocales = ['en', 'es', 'fr', 'de', 'ja'];
return supportedLocales.includes(browserLang) ? browserLang : 'en';
};
const switchLocale = (newLocale: string): void => {
localStorage.setItem('preferredLocale', newLocale);
// Trigger re-render with new locale context
window.location.reload();
};
i18n Implementation Approach
Using react-i18next with:
- Language detection middleware
- Dynamic import of translation files
- Fallback to English for missing keys
- Pluralization support for video counts and durations
Translation File Organization
/src/locales/
├── en.json
├── es.json
├── fr.json
└── de.json
Component Integration Pattern
// i18n context provider
const I18nProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [currentLocale, setCurrentLocale] = useState<string>('en');
useEffect(() => {
const locale = detectLocale();
setCurrentLocale(locale);
}, []);
return (
<I18nContext.Provider value={{ currentLocale, switchLocale }}>
{children}
</I18nContext.Provider>
);
};
// Translation hook
const useTranslation = () => {
const { currentLocale } = useContext(I18nContext);
const t = useCallback((key: string, params?: Record<string, any>): string => {
const translation = translations[currentLocale]?.[key] || key;
if (params) {
return Object.entries(params).reduce((acc, [paramKey, value]) =>
acc.replace(`{{${paramKey}}}`, String(value)), translation);
}
return translation;
}, [currentLocale]);
return { t, currentLocale };
};
12. Monitoring & Observability
Real User Monitoring (RUM) Metrics to Track
Core Performance Indicators:
- Video Playback Start Time (LCP): Target <2.5s for 75% of users
- First Contentful Paint (FCP): Target <1.8s for 75% of users
- Time to Interactive (TTI): Target <3.0s for 75% of users
- Video Buffering Rate: Target <5% of playback sessions
- Video Resolution Switching Frequency: Track user preference patterns
User Behavior Metrics:
- Video completion rate (by 10% intervals)
- Average watch time per session
- Pause/resume patterns and frequency
- Seek behavior distribution (10s, 30s, 60s jumps)
- Device/browser compatibility breakdown
Error Tracking Integration
Error Capture Strategy:
- Integrate Sentry SDK with error grouping and user context
- Capture unhandled promise rejections and global errors
- Track video playback errors (buffering, decoding, network)
- Log client-side performance errors (memory leaks, excessive re-renders)
Error Classification:
- Critical: Video player crashes, playback failure
- Warning: Buffering timeouts, quality degradation
- Info: User interaction errors, non-critical UI issues
Custom Events and User Journey Tracking
Key User Journeys:
// Event tracking interface
interface UserEvent {
eventName: string;
userId?: string;
sessionId?: string;
timestamp: number;
properties?: Record<string, any>;
}
// Example tracking events
const VideoPlayerEvents = {
VIDEO_PLAY_STARTED: 'video_play_started',
VIDEO_PAUSED: 'video_paused',
VIDEO_RESUMED: 'video_resumed',
VIDEO_COMPLETED: 'video_completed',
QUALITY_CHANGED: 'quality_changed',
SEEK_PERFORMED: 'seek_performed',
BUFFERING_STARTED: 'buffering_started',
BUFFERING_COMPLETED: 'buffering_completed'
};
Journey Tracking:
- Video browsing → selection → playback start → engagement metrics
- Search → filter → view → playback flow
- Recommendation → click → playback → completion
Performance Monitoring and Regression Detection
Performance Baseline Setup:
- Establish performance baselines per device category (mobile, tablet, desktop)
- Set up automated regression alerts for >10% degradation in key metrics
- Monitor CPU and memory usage during video playback (target <500MB RAM)
Monitoring Dashboard:
- Real-time metrics dashboard with alerting thresholds
- Historical trend analysis for performance improvements
- Geographic and device-specific performance breakdowns
Automated Alerting:
- Buffering rate >15% for 5-minute window triggers alert
- Playback failure rate >2% in 1-hour period
- Page load time >5s for 75th percentile users
graph TD
A[User Interaction] --> B{Performance Metrics}
B --> C[Real User Monitoring]
B --> D[Error Tracking]
C --> E[Core Web Vitals]
D --> F[Sentry Integration]
E --> G[Dashboard Alerts]
F --> H[Error Classification]
13. File & Folder Structure
Proposed Directory Structure
/src
/components
/video-player
VideoPlayer.tsx
VideoControls.tsx
VideoProgressBar.tsx
VideoSettingsModal.tsx
/video-grid
VideoGrid.tsx
VideoCard.tsx
VideoSkeleton.tsx
/ui
Button.tsx
Modal.tsx
Spinner.tsx
SearchInput.tsx
/features
/video-streaming
VideoStreamingPage.tsx
VideoStreamingProvider.tsx
useVideoStreaming.ts
/search
SearchPage.tsx
SearchResults.tsx
useSearch.ts
/watch-history
WatchHistoryPage.tsx
useWatchHistory.ts
/hooks
useVideoPlayer.ts
useVideoCache.ts
useScrollPosition.ts
/services
videoApi.ts
cacheService.ts
/types
video.ts
user.ts
/utils
formatTime.ts
debounce.ts
storage.ts
/assets
icons/
images/
/styles
globals.css
theme.ts
Module Boundaries and Import Rules
- Feature isolation: Each feature (video-streaming, search, watch-history) is contained in its own directory with clear boundaries
- No cross-feature imports: Components in
/features/video-streamingcannot directly import from/features/search - Shared components: UI primitives like
Button,Modallive in/components/uiand are safely imported across features - Barrel exports: Feature directories have
index.tsexports for clean imports
Shared vs. Feature-Specific Code Separation
- Shared code (utils, types, UI components) lives in top-level directories to avoid duplication
- Feature-specific code (page components, custom hooks) is isolated within their feature directories to maintain clean separation of concerns
- State management is centralized in
/hooksand/featuresto avoid tight coupling between components
This structure supports scalable development with clear ownership boundaries while maintaining reusability of common UI elements and utility functions.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. React (v18+)
- Justification: Core UI library with concurrent rendering, hooks, and seamless integration with modern React ecosystem
- Bundle impact: Minimal when properly tree-shaken
- Alternatives considered: Preact - smaller bundle but less ecosystem support and React compatibility concerns
2. TypeScript (v5+)
- Justification: Strong typing for better developer experience, compile-time error detection, and improved IDE support
- Bundle impact: None (compile-time only)
- Alternatives considered: Flow - less adoption in modern React ecosystem
3. React Router v6+
- Justification: Declarative routing with nested routes, dynamic route parameters, and excellent integration with React
- Bundle impact: ~20KB (minified)
- Alternatives considered: Next.js - not suitable for custom video streaming UI with complex routing needs
4. React Query v5+
- Justification: Production-ready data fetching with built-in caching, background updates, and automatic revalidation
- Bundle impact: ~15KB (minified)
- Alternatives considered: Redux Toolkit - more boilerplate for simple data fetching scenarios
5. Tailwind CSS v3+
- Justification: Utility-first CSS framework with excellent customization capabilities and zero-runtime CSS generation
- Bundle impact: ~10KB (minified)
- Alternatives considered: Styled Components - more boilerplate and less performance for simple UIs
6. Framer Motion v10+
- Justification: Production-grade animation library with excellent performance and declarative API for UI transitions
- Bundle impact: ~25KB (minified)
- Alternatives considered: CSS animations - less maintainable for complex UI interactions
7. React Hook Form v7+
- Justification: High-performance form handling with built-in validation and optimal re-renders
- Bundle impact: ~15KB (minified)
- Alternatives considered: Formik - more verbose and less performant for complex forms
8. Axios v1+
- Justification: Robust HTTP client with request/response interception, automatic JSON parsing, and error handling
- Bundle impact: ~20KB (minified)
- Alternatives considered: Fetch API - requires more boilerplate for interceptors and error handling
9. Video.js v8+
- Justification: Industry-standard video player with extensive browser support, extensibility, and accessibility features
- Bundle impact: ~50KB (minified)
- Alternatives considered: HTML5 video - lacks advanced features and consistent cross-browser support
10. react-window v1+
- Justification: High-performance virtualization for large lists with minimal DOM overhead
- Bundle impact: ~15KB (minified)
- Alternatives considered: react-virtual - similar performance but less mature ecosystem
Alternatives Considered and Rejected
1. Next.js vs Custom Router
- Rejected: Video streaming UI requires fine-grained control over routing and navigation, including custom URL structures for video IDs and playback states
- Rationale: Next.js would introduce unnecessary complexity and limit customization for video-specific routing needs
2. Redux Toolkit vs Zustand
- Rejected: While Redux Toolkit offers mature ecosystem, Zustand provides simpler API for this specific use case with better performance characteristics
- Rationale: State management is primarily focused on video playback and content caching, not complex application state
3. Emotion vs Tailwind CSS
- Rejected: Emotion requires more configuration and has higher runtime overhead for simple UI components
- Rationale: Tailwind's utility-first approach aligns well with rapid prototyping and design system integration
4. Material UI vs Custom Components
- Rejected: MUI would introduce unnecessary bloat and complexity for a video streaming UI with custom design requirements
- Rationale: Custom component approach allows for perfect alignment with video streaming brand and performance requirements
Licensing and Bundle Size Impact Assessment
Bundle Size Budget Analysis:
- Core UI Libraries (~100KB): React + React Router + React Query + Tailwind CSS
- Animation & Interactions (~40KB): Framer Motion + React Hook Form
- Media Handling (~75KB): Video.js + react-window
- HTTP Client (~20KB): Axios
Total Estimated Bundle Size: ~235KB (minified + gzipped) for core functionality
License Compliance:
- All selected libraries are MIT or BSD licensed, suitable for commercial video streaming applications
- Video.js has a permissive license compatible with commercial use
- React Query and Framer Motion have permissive licenses with no restrictions for video streaming applications
Tree-shaking Strategy:
- All libraries support tree-shaking to minimize bundle size
- React Query and Framer Motion are configured for minimal imports
- Video.js is imported with only required components to reduce bundle size
Performance Considerations
Bundle Size Optimization:
- React Query provides automatic caching and deduplication to reduce network requests
- Video.js is loaded asynchronously to avoid blocking initial page load
- Tailwind CSS is configured with purge to remove unused styles
Lazy Loading Strategy:
- Video.js player component is loaded only when needed (on video play)
- Large list virtualization is implemented with react-window for optimal rendering performance
- Route-based code splitting ensures minimal initial bundle size
Third-party Integration:
- All third-party libraries are configured with proper error boundaries and fallbacks
- Video.js integration includes graceful degradation for unsupported browsers
- React Query handles automatic retry and caching strategies for optimal user experience
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Content licensing model: Whether to support DRM-protected content vs. public streaming, which impacts authentication complexity and security requirements
- Video quality tier selection: Whether to auto-adapt or allow user choice, and how many quality levels to support (2-4 tiers recommended)
- Offline playback strategy: Whether to support download capabilities and storage constraints for mobile vs. web
- Ad integration approach: Whether to embed ads in player or use ad-free premium tiers, affecting UI complexity and revenue model
Known trade-offs made in this design
- Performance vs. feature richness: Prioritized core streaming experience over advanced features like multi-camera support or real-time chat to maintain performance targets
- State management complexity vs. scalability: Chose React Context + Zustand over Redux Toolkit for simpler setup, accepting potential scaling challenges with complex state interactions
- Bundle size vs. feature completeness: Used component-based code splitting over monolithic bundles to reduce initial load, accepting some duplication in shared components
- User experience vs. accessibility: Prioritized mainstream UX patterns over edge-case accessibility scenarios, with proper ARIA implementation as fallback
Risks and mitigations
- Network instability: Risk of playback interruptions during poor connectivity. Mitigation: Implement adaptive bitrate streaming with automatic quality switching and prefetching strategies
- Memory leaks in video playback: Risk of browser memory accumulation during long sessions. Mitigation: Implement proper cleanup of video elements and resource disposal in component lifecycle methods
- Content delivery latency: Risk of content not loading quickly enough for user expectations. Mitigation: Use CDN with edge locations and implement prefetching for next content items
- Cross-browser compatibility: Risk of inconsistent video playback across browsers. Mitigation: Implement browser feature detection and fallback strategies with graceful degradation
Technical debt acknowledged
- Player abstraction layer: Current implementation assumes single video player type, but future expansion to multiple player types (e.g., HLS vs. DASH) will require refactoring of the media controller layer
- State normalization: While normalized state is implemented, complex nested state relationships may require more sophisticated normalization patterns as feature set grows
- Component reusability: Some components like video player and playlist are currently tightly coupled to specific use cases, making them less reusable for other content types
- Performance monitoring: Current implementation lacks comprehensive RUM tracking and error logging integration, requiring future addition of observability tooling