High-Level Design: Video Streaming Platform (YouTube-like)
Table of Contents
- Problem Statement & Requirements
- High-Level Architecture
- Component Architecture
- Data Flow
- API Design & Communication Protocols
- Database Design
- Caching Strategy
- State Management
- Performance Optimization
- Error Handling & Edge Cases
- Interview Cross-Questions
- Trade-offs & Design Decisions
- Accessibility (a11y)
- Security & Content Protection
- Mobile & Touch Interactions
- Testing Strategy
- Offline/PWA Capabilities
- Video Player Deep Dive
- Internationalization (i18n)
- Analytics & Monitoring
- Notification System
- Live Streaming Deep Dive
1. Problem Statement & Requirements
Functional Requirements
- Video Upload: Users can upload videos in various formats and sizes
- Video Streaming: Users can watch videos with adaptive quality
- Video Player Controls: Play, pause, seek, volume, quality selection, speed control
- Comments: Users can post, read, edit, and delete comments
- Recommendations: Personalized video suggestions based on viewing history
- Search: Search videos by title, tags, description
- Thumbnails: Auto-generate and custom upload thumbnails
- View Counting: Track video views accurately (eventual consistency acceptable)
- Live Streaming: Support for real-time video streaming (optional)
- Subscriptions: Users can subscribe to channels
- Likes/Dislikes: User engagement metrics
Non-Functional Requirements
- Scalability: Support millions of concurrent viewers
- Availability: 99.9% uptime for streaming service
- Low Latency: Video start time < 2 seconds, buffering minimal
- Consistency: Eventual consistency acceptable for views, likes
- Durability: Videos should never be lost (high durability)
- Performance: Support adaptive bitrate streaming (240p to 4K)
- Cost Efficiency: Optimize storage and bandwidth costs
- Global Reach: CDN-based delivery for worldwide users
Scale Estimations
- Users: 500M daily active users
- Videos: 500 hours of video uploaded per minute
- Concurrent Viewers: 10M concurrent video streams
- Storage: 1PB+ for video storage (multi-resolution)
- Bandwidth: 100+ Gbps for video delivery
- Metadata: Billions of video records, comments, likes
2. High-Level Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Web App │ │ Mobile App │ │ Smart TV │ │
│ │ (React/Vue) │ │ (iOS/Android)│ │ App │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼──────────────────┼──────────────────┼──────────────────────────┘
│ │ │
└──────────────────┼──────────────────┘
│
┌────────▼────────┐
│ API Gateway │
│ (Rate Limiting,│
│ Auth, Routing)│
└────────┬────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌──────▼──────┐ ┌─────▼──────┐ ┌─────▼──────┐
│ Video │ │ Metadata │ │ User │
│ Upload │ │ Service │ │ Service │
│ Service │ │ │ │ │
└──────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
│ │ │
┌──────▼──────┐ ┌─────▼──────┐ ┌─────▼──────┐
│ Transcode │ │ Comment │ │ Recommend. │
│ Service │ │ Service │ │ Service │
│ (Queue) │ │ │ │ (ML) │
└──────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
│ │ │
┌─────────▼─────────────────▼──────────────────▼─────────────┐
│ DATA LAYER │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ SQL │ │ NoSQL │ │ Object │ │ Cache │ │
│ │ (RDS) │ │(Cassandra│ │ Storage │ │ (Redis) │ │
│ │ │ │/DynamoDB)│ │ (S3) │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ CDN LAYER │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ CDN Edge│ │ CDN Edge│ │ CDN Edge│ │ CDN Edge│ │
│ │ (US) │ │ (EU) │ │ (APAC) │ │ (Others)│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ BACKGROUND JOBS │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Thumbnail │ │ View │ │Analytics │ │ CDN │ │
│ │Generator │ │ Counter │ │Processor │ │ Warmer │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
2.0.a System Invariants & Frontend Consistency Model
A video streaming frontend is a distributed system between:
- Browser
- CDN
- Player runtime
- Backend control plane
The UI must enforce correctness even when these components are temporarily inconsistent.
These invariants must never be violated.
2.0.b Playback State Invariants
| Invariant | Why it must hold |
|---|---|
| UI must never show “Playing” if no media bytes are being consumed | Prevents fake playback |
| UI must never advance timestamp if buffer is empty | Prevents desync |
| Video time must be monotonic except during user seeks | Prevents jumps |
| Pause must always stop network fetch | Prevents waste + billing errors |
| A stalled network must never lock UI | Prevents dead-UI |
These are enforced by the player state machine.
2.0.c Buffering Invariants
The buffer is the frontend’s source of truth for playback.
Rules:
- Playback can start only when buffer ≥ minimum threshold
- Playback must pause automatically when buffer = 0
- Video must never outrun buffer
- Buffer eviction must never remove currently playing segment
Violation of these rules causes:
- Infinite spinner bugs
- Ghost playback
- Desync between audio and video
2.0.d Manifest Consistency
The manifest is the contract between backend and player.
The frontend enforces:
| Rule | Why |
|---|---|
| Manifest must always match codec in buffer | Prevents decode failure |
| Manifest refresh must be idempotent | Prevents rewind bugs |
| Manifest updates must not invalidate already-buffered segments | Prevents playback drop |
Frontend always treats manifest as append-only during active playback.
2.0.e UI vs Network Truth
The UI does not trust the network.
The UI derives state from:
- Player buffer
- Decoder state
- Playback clock
Not from:
- CDN responses
- Backend playback metadata
This prevents:
- “Video says playing but nothing plays”
- Broken pause/play buttons
- Wrong seek bar
2.0.f Live Stream Invariants
For live video:
- The playback head must never go beyond live edge
- The UI must expose latency vs quality tradeoff
- Rebuffering must not rewind live edge
Live video correctness is harder than VOD.
The frontend enforces:
- Drift control
- Jitter smoothing
- Live-edge correction
5. Frontend Failure Model
The frontend assumes failure is normal.
5.1 CDN Failure
If a video segment fails to load:
- Retry same CDN
- Try alternate CDN
- Drop to lower bitrate
- Pause playback if buffer hits zero
The UI never shows “playing” unless buffer is moving.
5.2 Manifest Failure
If manifest refresh fails:
- Continue playing buffered segments
- Freeze bitrate adaptation
- Retry in background
Playback should not drop just because control plane failed.
5.3 Network Degradation
The frontend continuously measures:
- Download speed
- Buffer growth
- Rebuffer frequency
It dynamically:
- Reduces quality
- Increases buffer targets
- Disables prefetch
This keeps playback stable instead of pretty.
5.4 Tab Suspension / Mobile App Backgrounding
If browser or app is suspended:
- Freeze playback clock
- Save buffer state
- Resume without reloading
Avoids restarting video.
5.5 User Actions During Failure
Users may:
- Seek while buffering
- Pause during network loss
- Switch quality mid-segment
The frontend serializes these actions and applies them when safe.
No user input is lost.
3. Component Architecture
3.1 Frontend Components
┌────────────────────────────────────────────────────────┐
│ VIDEO PLAYER COMPONENT │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Video Rendering Canvas │ │
│ │ (HTML5 Video / WebRTC for live) │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Player Controls │ │
│ │ [Play/Pause] [Timeline] [Volume] [Quality] │ │
│ │ [Speed] [Fullscreen] [Captions] [Settings] │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Adaptive Bitrate Logic │ │
│ │ - Monitor bandwidth & buffer health │ │
│ │ - Switch quality based on network │ │
│ │ - Preload next segments │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Buffer Management │ │
│ │ - Maintain 10-30s buffer ahead │ │
│ │ - Handle network interruptions │ │
│ │ - Smart preloading │ │
│ └───────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ RECOMMENDATION COMPONENT │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Video │ │ Video │ │ Video │ │
│ │ Thumbnail│ │ Thumbnail│ │ Thumbnail│ ... │
│ │ + Meta │ │ + Meta │ │ + Meta │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ - Personalized based on watch history │
│ - Trending videos │
│ - Category-based recommendations │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ COMMENTS COMPONENT │
│ ┌──────────────────────────────────────────────┐ │
│ │ Comment Input (with mentions, emojis) │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Comment List (paginated/infinite scroll) │ │
│ │ - Top comments │ │
│ │ - Newest first │ │
│ │ - Threaded replies │ │
│ └──────────────────────────────────────────────┘ │
│ - Real-time updates (WebSocket/Polling) │
│ - Like/Dislike comments │
└────────────────────────────────────────────────────────┘
3.1.a Frontend Failure Model
The frontend assumes failure is normal.
3.1.b CDN Failure
If a video segment fails to load:
- Retry same CDN
- Try alternate CDN
- Drop to lower bitrate
- Pause playback if buffer hits zero
The UI never shows “playing” unless buffer is moving.
3.1.c Manifest Failure
If manifest refresh fails:
- Continue playing buffered segments
- Freeze bitrate adaptation
- Retry in background
Playback should not drop just because control plane failed.
3.1.d Network Degradation
The frontend continuously measures:
- Download speed
- Buffer growth
- Rebuffer frequency
It dynamically:
- Reduces quality
- Increases buffer targets
- Disables prefetch
This keeps playback stable instead of pretty.
3.1.e Tab Suspension / Mobile App Backgrounding
If browser or app is suspended:
- Freeze playback clock
- Save buffer state
- Resume without reloading
Avoids restarting video.
3.1.f User Actions During Failure
Users may:
- Seek while buffering
- Pause during network loss
- Switch quality mid-segment
The frontend serializes these actions and applies them when safe.
No user input is lost.
3.2 Backend Services
Video Upload Service
┌─────────────────────────────────────────┐
│ VIDEO UPLOAD SERVICE │
│ │
│ 1. Validate upload (format, size) │
│ 2. Generate unique video ID │
│ 3. Chunk upload to Object Storage │
│ 4. Create metadata entry │
│ 5. Trigger transcoding job │
│ 6. Generate placeholder thumbnail │
│ │
│ Technologies: │
│ - Multipart upload (chunks) │
│ - Resumable uploads │
│ - S3/GCS for raw video storage │
└─────────────────────────────────────────┘
Transcoding Service
┌─────────────────────────────────────────┐
│ TRANSCODING SERVICE │
│ │
│ Input: Raw video file │
│ │
│ Process: │
│ 1. Read from Object Storage │
│ 2. Transcode to multiple resolutions: │
│ - 4K (2160p) │
│ - 1080p │
│ - 720p │
│ - 480p │
│ - 360p │
│ - 240p │
│ 3. Encode with H.264/H.265/VP9 │
│ 4. Generate HLS/DASH manifests │
│ 5. Extract thumbnails (keyframes) │
│ 6. Store segments in Object Storage │
│ 7. Update metadata with URLs │
│ 8. Warm CDN cache │
│ │
│ Technologies: │
│ - FFmpeg/Elastic Transcoder │
│ - Worker queue (SQS/RabbitMQ) │
│ - Distributed workers (Auto-scaling) │
└─────────────────────────────────────────┘
Streaming Service
┌─────────────────────────────────────────┐
│ STREAMING SERVICE │
│ │
│ 1. Receive video request │
│ 2. Check user authentication │
│ 3. Fetch manifest file (HLS/DASH) │
│ 4. Serve via CDN │
│ 5. Track playback events │
│ 6. Log analytics │
│ │
│ Protocols: │
│ - HLS (HTTP Live Streaming) │
│ - DASH (Dynamic Adaptive Streaming) │
│ - WebRTC (for live streaming) │
└─────────────────────────────────────────┘
4. Data Flow
4.1 Video Upload Flow
┌─────────┐
│ User │
└────┬────┘
│ 1. Upload Video
▼
┌─────────────────┐
│ Upload Service │
└────┬────────────┘
│ 2. Chunk & Store Raw Video
▼
┌─────────────────┐
│ Object Storage │
│ (S3) │
└────┬────────────┘
│ 3. Trigger Transcode
▼
┌─────────────────┐
│ Message Queue │
│ (SQS/Kafka) │
└────┬────────────┘
│ 4. Pick Job
▼
┌─────────────────┐
│ Transcoder │
│ Workers │
└────┬────────────┘
│ 5. Generate Multiple Resolutions
├──────────┬──────────┬──────────┐
▼ ▼ ▼ ▼
[4K.m3u8] [1080p.m3u8] [720p.m3u8] [360p.m3u8]
│ │ │ │
└──────────┴──────────┴──────────┘
│
▼
┌─────────────────┐
│ Object Storage │
│ (Segmented) │
└────┬────────────┘
│ 6. Update Metadata
▼
┌─────────────────┐
│ Database │
│ (video_id, │
│ resolutions) │
└────┬────────────┘
│ 7. Warm CDN
▼
┌─────────────────┐
│ CDN │
│ Edge Servers │
└─────────────────┘
4.2 Video Streaming Flow
┌─────────┐
│ User │
└────┬────┘
│ 1. Request Video
▼
┌─────────────────┐
│ API Gateway │
└────┬────────────┘
│ 2. Auth & Validate
▼
┌─────────────────┐
│ Metadata Service│
└────┬────────────┘
│ 3. Fetch Video Info
▼
┌─────────────────┐
│ Database │
└────┬────────────┘
│ 4. Return Manifest URL
▼
┌─────────────────┐
│ Client │
│ (Video Player) │
└────┬────────────┘
│ 5. Request Manifest (master.m3u8)
▼
┌─────────────────┐
│ CDN Edge │
└────┬────────────┘
│ 6. Serve Manifest
▼
┌─────────────────┐
│ Client │
│ (Parse Quality)│
└────┬────────────┘
│ 7. Request Segments (720p_001.ts, 720p_002.ts...)
▼
┌─────────────────┐
│ CDN Edge │
│ (Cache Hit) │
└────┬────────────┘
│ 8. Stream Video Segments
▼
┌─────────────────┐
│ Video Player │
│ (Buffer & Play)│
└─────────────────┘
4.3 Adaptive Bitrate Flow
Video Player Logic:
┌─────────────────────────────────────────┐
│ │
│ while (video playing): │
│ monitor_bandwidth() │
│ monitor_buffer_health() │
│ │
│ if bandwidth_high && buffer_healthy: │
│ switch_to_higher_quality() │
│ if bandwidth_low || buffer_starving: │
│ switch_to_lower_quality() │
│ │
│ preload_next_segments() │
│ update_playback_stats() │
│ │
└─────────────────────────────────────────┘
Quality Ladder:
4K (2160p) ─┐
1080p ─┤
720p ─┼─── Switch based on:
480p ─┤ - Network speed
360p ─┤ - Buffer status
240p ─┘ - Device capability
4.4 View Counting Flow
┌─────────┐
│ User │
│ Watches │
└────┬────┘
│ 1. Video Play Event (>30s watched)
▼
┌─────────────────┐
│ Analytics │
│ Service │
└────┬────────────┘
│ 2. Write to Stream
▼
┌─────────────────┐
│ Kafka/Kinesis │
│ (Event Log) │
└────┬────────────┘
│ 3. Aggregate Views
▼
┌─────────────────┐
│ Stream │
│ Processor │
│ (Flink/Spark) │
└────┬────────────┘
│ 4. Batch Update (every 5 min)
▼
┌─────────────────┐
│ Redis Counter │
│ (Fast Reads) │
└────┬────────────┘
│ 5. Periodic Flush
▼
┌─────────────────┐
│ Database │
│ (Persistent) │
└─────────────────┘
Note: Eventual consistency is acceptable
Views may be slightly delayed (5-10 min)
5. API Design & Communication Protocols
5.1 REST APIs
Video Metadata APIs
GET /api/v1/videos/{videoId}
Response:
{
"videoId": "abc123",
"title": "Sample Video",
"description": "...",
"duration": 600,
"views": 1000000,
"likes": 50000,
"dislikes": 1000,
"channelId": "channel123",
"uploadDate": "2025-01-15T10:00:00Z",
"thumbnailUrl": "https://cdn.example.com/thumbnails/abc123.jpg",
"manifestUrl": "https://cdn.example.com/videos/abc123/master.m3u8",
"resolutions": ["2160p", "1080p", "720p", "480p", "360p", "240p"]
}
POST /api/v1/videos/upload
Headers:
Authorization: Bearer
Body (multipart):
file:
title: "Video Title"
description: "..."
tags: ["tag1", "tag2"]
Response:
{
"videoId": "abc123",
"uploadStatus": "processing",
"uploadUrl": "https://upload.example.com/abc123"
}
GET /api/v1/videos/{videoId}/recommendations
Response:
{
"videos": [
{"videoId": "xyz789", "title": "...", "thumbnailUrl": "..."},
...
]
}
Comment APIs
POST /api/v1/videos/{videoId}/comments
Headers:
Authorization: Bearer <token>
Body:
{
"text": "Great video!",
"parentCommentId": null
}
Response:
{
"commentId": "comment123",
"userId": "user456",
"text": "Great video!",
"createdAt": "2025-01-15T10:00:00Z"
}
GET /api/v1/videos/{videoId}/comments?limit=20&offset=0&sort=top
Response:
{
"comments": [
{
"commentId": "comment123",
"userId": "user456",
"userName": "John Doe",
"text": "Great video!",
"likes": 100,
"replies": 5,
"createdAt": "2025-01-15T10:00:00Z"
},
...
],
"nextOffset": 20
}
User Engagement APIs
POST /api/v1/videos/{videoId}/like
POST /api/v1/videos/{videoId}/dislike
POST /api/v1/channels/{channelId}/subscribe
POST /api/v1/videos/{videoId}/watch
Body: { "timestamp": 120, "quality": "720p" }
5.2 Streaming Protocols
HLS (HTTP Live Streaming)
Master Playlist (master.m3u8):
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=3840x2160
2160p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=854x480
480p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
360p/index.m3u8
Quality Playlist (720p/index.m3u8):
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXTINF:10.0,
segment_001.ts
#EXTINF:10.0,
segment_002.ts
#EXTINF:10.0,
segment_003.ts
...
#EXT-X-ENDLIST
DASH (Dynamic Adaptive Streaming over HTTP)
MPD (Media Presentation Description):
mimeType="video/mp4">
bandwidth="8000000" width="3840" height="2160">
2160p/
/>
bandwidth="5000000" width="1920" height="1080">
1080p/
/>
...
5.3 WebSocket (Real-time Updates)
// Comment updates
WS /ws/videos/{videoId}/comments
Client -> Server:
{
"type": "subscribe",
"videoId": "abc123"
}
Server -> Client (new comment):
{
"type": "new_comment",
"comment": {
"commentId": "comment789",
"userId": "user123",
"text": "Just posted!",
"createdAt": "2025-01-15T10:05:00Z"
}
}
// Live view count updates
Server -> Client (every 10s):
{
"type": "view_count_update",
"views": 1000543
}
6. Database Design
6.1 SQL Database (MySQL/PostgreSQL) – Metadata
Videos Table
CREATE TABLE videos (
video_id VARCHAR(36) PRIMARY KEY,
channel_id VARCHAR(36) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
duration INT NOT NULL, -- in seconds
upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status ENUM('processing', 'ready', 'failed') DEFAULT 'processing',
category_id INT,
privacy ENUM('public', 'unlisted', 'private') DEFAULT 'public',
manifest_url VARCHAR(512),
thumbnail_url VARCHAR(512),
raw_video_url VARCHAR(512),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_channel_id (channel_id),
INDEX idx_upload_date (upload_date),
INDEX idx_status (status),
INDEX idx_category (category_id)
);
CREATE TABLE video_resolutions (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
video_id VARCHAR(36) NOT NULL,
resolution VARCHAR(10) NOT NULL, -- '720p', '1080p', etc.
video_url VARCHAR(512) NOT NULL,
bitrate INT,
file_size BIGINT,
FOREIGN KEY (video_id) REFERENCES videos(video_id),
INDEX idx_video_id (video_id)
);
CREATE TABLE video_stats (
video_id VARCHAR(36) PRIMARY KEY,
view_count BIGINT DEFAULT 0,
like_count INT DEFAULT 0,
dislike_count INT DEFAULT 0,
comment_count INT DEFAULT 0,
share_count INT DEFAULT 0,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (video_id) REFERENCES videos(video_id)
);
Channels Table
CREATE TABLE channels (
channel_id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
channel_name VARCHAR(100) NOT NULL,
description TEXT,
subscriber_count BIGINT DEFAULT 0,
total_views BIGINT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id)
);
CREATE TABLE subscriptions (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
channel_id VARCHAR(36) NOT NULL,
subscribed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
notifications_enabled BOOLEAN DEFAULT TRUE,
UNIQUE KEY unique_subscription (user_id, channel_id),
INDEX idx_user_id (user_id),
INDEX idx_channel_id (channel_id)
);
Users Table
CREATE TABLE users (
user_id VARCHAR(36) PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100),
profile_picture_url VARCHAR(512),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
INDEX idx_email (email),
INDEX idx_username (username)
);
6.2 NoSQL Database (Cassandra/DynamoDB) – Comments & Engagement
Comments Table (Cassandra Schema)
CREATE TABLE comments (
video_id TEXT,
comment_id TIMEUUID,
user_id TEXT,
parent_comment_id TIMEUUID,
text TEXT,
like_count INT,
created_at TIMESTAMP,
updated_at TIMESTAMP,
PRIMARY KEY (video_id, comment_id)
) WITH CLUSTERING ORDER BY (comment_id DESC);
-- Secondary index for user comments
CREATE TABLE user_comments (
user_id TEXT,
comment_id TIMEUUID,
video_id TEXT,
text TEXT,
created_at TIMESTAMP,
PRIMARY KEY (user_id, comment_id)
) WITH CLUSTERING ORDER BY (comment_id DESC);
-- Index for comment replies
CREATE TABLE comment_replies (
parent_comment_id TIMEUUID,
reply_id TIMEUUID,
user_id TEXT,
text TEXT,
created_at TIMESTAMP,
PRIMARY KEY (parent_comment_id, reply_id)
) WITH CLUSTERING ORDER BY (reply_id DESC);
Watch History (Cassandra Schema)
CREATE TABLE watch_history (
user_id TEXT,
watched_at TIMESTAMP,
video_id TEXT,
watch_duration INT, -- seconds watched
total_duration INT, -- video total duration
quality TEXT, -- resolution watched
PRIMARY KEY (user_id, watched_at, video_id)
) WITH CLUSTERING ORDER BY (watched_at DESC);
-- For recommendations
CREATE TABLE user_preferences (
user_id TEXT PRIMARY KEY,
favorite_categories SET<TEXT>,
disliked_categories SET<TEXT>,
preferred_languages SET<TEXT>,
watch_time_total BIGINT,
last_updated TIMESTAMP
);
Video Analytics (Time-Series Data)
CREATE TABLE video_analytics (
video_id TEXT,
time_bucket TIMESTAMP, -- hourly/daily buckets
metric_type TEXT, -- 'views', 'watch_time', 'engagement'
value BIGINT,
PRIMARY KEY ((video_id, metric_type), time_bucket)
) WITH CLUSTERING ORDER BY (time_bucket DESC);
6.3 Redis (Caching Layer)
# Video metadata cache
Key: video:{videoId}
Value: {JSON of video metadata}
TTL: 1 hour
# Video stats cache (hot data)
Key: video:stats:{videoId}
Value: {views: 1000000, likes: 50000, ...}
TTL: 5 minutes
# Trending videos cache
Key: trending:videos:{region}:{category}
Value: [videoId1, videoId2, ...]
TTL: 10 minutes
# User session cache
Key: user:session:{sessionId}
Value: {userId, preferences, ...}
TTL: 24 hours
# View count buffer (before batch update)
Key: video:views:{videoId}
Value: counter (incremented on each view)
Flush to DB: every 5 minutes
# Comment count cache
Key: video:comments:{videoId}
Value: sorted set of recent comments
TTL: 15 minutes
# Recommendation cache
Key: recommendations:{userId}
Value: [videoId1, videoId2, ...]
TTL: 30 minutes
7. Caching Strategy
7.1 Multi-Layer Caching Architecture
┌──────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ Browser Cache: Video segments (24h) │
│ IndexedDB: Offline video chunks │
└─────────────────┬────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ CDN EDGE CACHE │
│ - Video segments (HLS/DASH): 7 days │
│ - Thumbnails: 30 days │
│ - Popular videos: Indefinite (with LRU) │
│ - Cache hit ratio target: >95% │
└─────────────────┬────────────────────────────────────┘
│ (Cache miss)
▼
┌──────────────────────────────────────────────────────┐
│ APPLICATION CACHE (Redis) │
│ - Video metadata: 1 hour │
│ - User sessions: 24 hours │
│ - Trending videos: 10 minutes │
│ - View counts: 5 minutes │
│ - Recommendations: 30 minutes │
└─────────────────┬────────────────────────────────────┘
│ (Cache miss)
▼
┌──────────────────────────────────────────────────────┐
│ DATABASE LAYER │
│ SQL: Metadata, User data │
│ NoSQL: Comments, Analytics │
│ Object Storage: Video files │
└──────────────────────────────────────────────────────┘
7.2 CDN Strategy
Cache-Control Headers
Video Segments (*.ts, *.m4s):
Cache-Control: public, max-age=604800, immutable
(7 days, never changes once created)
Manifest Files (*.m3u8, *.mpd):
Cache-Control: public, max-age=60
(1 minute, can update for live streams)
Thumbnails:
Cache-Control: public, max-age=2592000
(30 days)
Video Metadata API:
Cache-Control: public, max-age=300
(5 minutes)
CDN Optimization Techniques
1. Geo-Distributed Edge Servers
- User routed to nearest edge location
- Reduces latency from ~200ms to ~20ms
2. Cache Warming
- Pre-populate CDN with popular videos
- Triggered on viral video detection
3. Range Requests
- Support HTTP byte-range requests
- Enable seeking without downloading entire file
4. Compression
- Gzip/Brotli for manifests and metadata
- Video already compressed (H.264/H.265)
5. Cache Tiering
- Hot tier: Most popular videos (SSD)
- Warm tier: Moderately popular (HDD)
- Cold tier: Long-tail (Origin fetch)
7.3 Cache Invalidation Strategy
1. Time-Based Expiration (TTL)
- Most common approach
- Acceptable for view counts, stats
2. Event-Based Invalidation
- Video deleted -> Invalidate all CDN cache
- Video updated -> Invalidate metadata cache
- Comment posted -> Invalidate comment cache
3. Write-Through Cache
- Update cache immediately on write
- Used for critical data (likes, subscriptions)
4. Cache-Aside Pattern
- Application checks cache first
- On miss, fetch from DB and populate cache
- Used for video metadata
8. State Management
8.1 Client-Side State (Video Player)
// Video Player State Machine
{
playbackState: 'playing' | 'paused' | 'buffering' | 'ended',
currentTime: 120.5, // seconds
duration: 600,
bufferedRanges: [[0, 150], [200, 250]], // buffered segments
currentQuality: '720p',
availableQualities: ['2160p', '1080p', '720p', '480p'],
volume: 0.8,
playbackRate: 1.0,
isFullscreen: false,
isMuted: false,
captionsEnabled: true,
// Adaptive bitrate state
networkBandwidth: 5000000, // bps
bufferHealth: 0.8, // 80% healthy
qualitySwitchPending: false,
// Analytics
watchedSegments: [0, 30, 60, 90], // timestamps watched
totalWatchTime: 150, // seconds
bufferingEvents: 2,
qualitySwitches: 3
}
8.2 Server-Side State
Session State (Redis)
{
"sessionId": "sess_abc123",
"userId": "user_456",
"videoId": "video_789",
"currentPosition": 120,
"quality": "720p",
"lastHeartbeat": "2025-01-15T10:05:00Z",
"device": "web",
"location": "US-WEST"
}
Transcoding Job State (DynamoDB)
{
"jobId": "job_xyz",
"videoId": "video_789",
"status": "processing",
"progress": 45,
"currentResolution": "720p",
"completedResolutions": ["240p", "360p", "480p"],
"pendingResolutions": ["1080p", "2160p"],
"startedAt": "2025-01-15T10:00:00Z",
"estimatedCompletion": "2025-01-15T10:15:00Z"
}
8.3 State Synchronization
User watches video on Mobile -> Switches to TV
1. Mobile app sends position update:
POST /api/v1/videos/{videoId}/progress
{ "position": 120, "timestamp": "..." }
2. Server updates Redis:
SET user:video:progress:{userId}:{videoId} "120"
3. TV app polls for progress:
GET /api/v1/videos/{videoId}/progress
Response: { "position": 120 }
4. TV resumes from 120 seconds
9. Performance Optimization
9.1 Video Player Optimizations
Preloading Strategy
// Intelligent preloading
function preloadStrategy(currentTime, duration, bufferHealth) {
const timeRemaining = duration - currentTime;
// Preload more if user is likely to finish video
if (timeRemaining < 60 && bufferHealth > 0.7) {
preloadAhead(30); // 30 seconds ahead
} else if (bufferHealth > 0.8) {
preloadAhead(20);
} else if (bufferHealth < 0.3) {
preloadAhead(10); // Conservative if buffer is low
}
// Preload next video in playlist
if (timeRemaining < 10) {
preloadNextVideo();
}
}
Adaptive Bitrate Algorithm
function selectQuality(bandwidth, bufferHealth, currentQuality) {
// Quality ladder (bitrates in bps)
const qualities = [
{ name: "240p", bitrate: 300000 },
{ name: "360p", bitrate: 800000 },
{ name: "480p", bitrate: 1400000 },
{ name: "720p", bitrate: 2800000 },
{ name: "1080p", bitrate: 5000000 },
{ name: "2160p", bitrate: 8000000 },
];
// Conservative switching (bandwidth * 0.8 safety factor)
const safeBandwidth = bandwidth * 0.8;
// Upscale if buffer is healthy and bandwidth supports
if (bufferHealth > 0.7) {
for (let i = qualities.length - 1; i >= 0; i--) {
if (safeBandwidth >= qualities[i].bitrate) {
return qualities[i].name;
}
}
}
// Downscale immediately if buffering
if (bufferHealth < 0.3) {
const currentIndex = qualities.findIndex((q) => q.name === currentQuality);
return qualities[Math.max(0, currentIndex - 1)].name;
}
return currentQuality;
}
Buffering Strategy
┌─────────────────────────────────────────────────┐
│ Buffer Management Strategy │
│ │
│ Initial Buffer: 5 seconds │
│ Target Buffer: 15-30 seconds │
│ Max Buffer: 60 seconds │
│ │
│ Rebuffering Threshold: < 3 seconds │
│ Quality Switch Threshold: < 10 seconds │
│ │
│ Buffer States: │
│ [0-3s] -> Critical (downscale quality) │
│ [3-10s] -> Low (maintain quality) │
│ [10-30s] -> Healthy (consider upscale) │
│ [30-60s] -> Optimal (upscale if possible) │
│ [60s+] -> Pause preloading │
└─────────────────────────────────────────────────┘
9.2 Thumbnail Optimization
1. Generate Multiple Thumbnails
- Sprite sheet (for seek preview)
- Default thumbnail (720p)
- Small thumbnail (360p for lists)
- WebP format (smaller size)
2. Lazy Loading
- Load thumbnails only when in viewport
- Intersection Observer API
- Placeholder blur image
3. Responsive Images
<img
srcset="thumb_360.webp 360w, thumb_720.webp 720w"
sizes="(max-width: 600px) 360px, 720px"
loading="lazy"
/>
9.3 Database Optimizations
Read Replicas
┌─────────────┐
│ Master │ (Writes: Upload, Update)
│ Database │
└──────┬──────┘
│
│ Replication
├───────────┬───────────┬───────────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Read │ │ Read │ │ Read │ │ Read │
│ Replica │ │ Replica │ │ Replica │ │ Replica │
│ (US) │ │ (EU) │ │ (APAC) │ │ (Other) │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
▲ ▲ ▲ ▲
│ │ │ │
[Read] [Read] [Read] [Read] [Read] [Read]
Video metadata, comments, recommendations
Database Indexing
-- Composite indexes for common queries
CREATE INDEX idx_video_channel_date
ON videos(channel_id, upload_date DESC);
CREATE INDEX idx_video_category_views
ON videos(category_id, view_count DESC);
-- Full-text search index
CREATE FULLTEXT INDEX idx_video_search
ON videos(title, description, tags);
-- Covering index (includes all columns needed)
CREATE INDEX idx_video_list
ON videos(status, category_id, upload_date DESC)
INCLUDE (video_id, title, thumbnail_url, duration);
Query Optimization
-- Bad: N+1 query problem
SELECT * FROM videos WHERE channel_id = 'xyz';
-- Then for each video:
SELECT * FROM video_stats WHERE video_id = ?;
-- Good: Single JOIN query
SELECT v.*, vs.view_count, vs.like_count
FROM videos v
LEFT JOIN video_stats vs ON v.video_id = vs.video_id
WHERE v.channel_id = 'xyz'
ORDER BY v.upload_date DESC
LIMIT 20;
-- Use pagination with cursor
SELECT * FROM videos
WHERE upload_date < '2025-01-15T10:00:00Z'
ORDER BY upload_date DESC
LIMIT 20;
9.4 Backend Optimizations
API Response Compression
Gzip compression for JSON responses
Reduces response size by 70-90%
Before: 50KB JSON
After: 5KB Gzipped
Headers:
Content-Encoding: gzip
Accept-Encoding: gzip, deflate, br
Database Connection Pooling
Connection Pool Settings:
Min Connections: 10
Max Connections: 100
Connection Timeout: 30s
Idle Timeout: 600s
Reuse connections instead of creating new ones
Reduces latency from ~100ms to ~5ms per query
Async Processing
Synchronous Upload Flow (Slow):
User -> Upload -> Transcode -> Store -> Return
Total: 10+ minutes
Asynchronous Upload Flow (Fast):
User -> Upload -> Queue Job -> Return
|
v
Background Worker
|
v
Transcode -> Store -> Notify
Total user wait: < 5 seconds
10. Error Handling & Edge Cases
10.1 Video Player Errors
// Comprehensive error handling
class VideoPlayerErrorHandler {
handleError(error) {
switch (error.code) {
case "MEDIA_ERR_ABORTED":
// User aborted playback
this.logError("User aborted", error);
break;
case "MEDIA_ERR_NETWORK":
// Network error during download
this.retryWithBackoff();
this.switchToLowerQuality();
this.showUserMessage("Network error. Retrying...");
break;
case "MEDIA_ERR_DECODE":
// Video decode error
this.switchToAlternateCodec();
this.reportCorruptVideo();
break;
case "MEDIA_ERR_SRC_NOT_SUPPORTED":
// Unsupported video format
this.showUserMessage("Video format not supported");
this.fallbackToFlashPlayer(); // Legacy support
break;
case "MANIFEST_LOAD_ERROR":
// HLS/DASH manifest failed to load
this.retryManifestLoad();
break;
case "SEGMENT_LOAD_ERROR":
// Individual segment failed
this.skipSegment();
this.continuePlayback();
break;
default:
this.showGenericError();
this.reportToMonitoring(error);
}
}
retryWithBackoff() {
const delays = [1000, 2000, 5000, 10000]; // ms
let attempt = 0;
const retry = () => {
if (attempt < delays.length) {
setTimeout(() => {
this.reloadVideo();
attempt++;
}, delays[attempt]);
} else {
this.showUserMessage("Unable to load video. Please try again later.");
}
};
retry();
}
}
10.2 Upload Failures
Upload Error Scenarios:
1. File Too Large (>10GB)
- Reject with clear error message
- Suggest video compression
- Return 413 Payload Too Large
2. Unsupported Format
- Check file extension and MIME type
- Return 415 Unsupported Media Type
- Provide list of supported formats
3. Network Interruption
- Resume upload from last chunk
- Store upload progress in Redis
- Support multipart upload resume
4. Storage Full
- Return 507 Insufficient Storage
- Queue for retry when space available
- Notify admins
5. Virus/Malware Detection
- Scan uploaded file
- Quarantine suspicious files
- Notify user and reject upload
10.3 Transcoding Failures
Transcoding Error Recovery:
1. Worker Failure
- Job returns to queue
- Another worker picks up
- Max retries: 3
2. Corrupted Video File
- Attempt repair with FFmpeg
- If repair fails, notify user
- Mark video as failed
3. Partial Transcoding Success
- 720p succeeds, 1080p fails
- Publish available resolutions
- Retry failed resolutions
4. Timeout (>1 hour)
- Split large video into chunks
- Transcode chunks in parallel
- Merge transcoded chunks
5. Resource Exhaustion
- Scale up worker pool
- Prioritize important videos
- Queue low-priority videos
10.4 CDN Failures
CDN Failure Scenarios:
1. Edge Server Down
- DNS failover to next nearest edge
- Fallback to origin server
- Auto-healing and alerting
2. Cache Corruption
- Invalidate corrupted cache
- Serve from origin
- Re-warm cache
3. Origin Server Unreachable
- Serve stale content (if acceptable)
- Use backup origin server
- Alert operations team
4. DDoS Attack
- Rate limiting at edge
- CAPTCHA for suspicious traffic
- Geo-blocking if needed
10.5 Edge Cases
Concurrent Video Edits
Problem: User uploads video, then immediately updates title/description
Solution:
1. Lock video metadata during initial processing
2. Queue metadata updates
3. Apply updates after processing completes
4. Use optimistic locking with version numbers
Deleted Video Still Cached
Problem: Video deleted but still accessible via CDN
Solution:
1. Immediate cache invalidation on delete
2. Purge CDN cache (max propagation: 5 min)
3. Add "video not found" check in origin
4. Return 404 even if cached (with short TTL)
View Count Inconsistency
Problem: Different view counts across regions
Solution:
1. Accept eventual consistency
2. Batch updates every 5 minutes
3. Use distributed counter (Redis)
4. Periodic reconciliation job
5. Show approximate counts ("1M+" instead of exact)
Live Stream to VOD Transition
Problem: Live stream ends, should become video-on-demand
Solution:
1. Detect stream end event
2. Concatenate live segments
3. Transcode to standard VOD formats
4. Update manifest from live to VOD
5. Archive chat replay alongside video
Seek in Unbuffered Region
Problem: User seeks to 5:00 but only 0:00-1:00 buffered
Solution:
1. Clear existing buffer
2. Request manifest for 5:00 timestamp
3. Load segments starting from 5:00
4. Show loading spinner during seek
5. Resume playback when buffered
11. Interview Cross-Questions
11.1 Scalability Questions
Q: How would you handle 10x traffic spike (e.g., breaking news)?
A: Multi-pronged approach:
- Auto-scaling: Horizontally scale services (API, transcoders, DB read replicas)
- CDN: Most traffic absorbed by CDN edge caches (95%+ hit rate)
- Rate Limiting: Protect backend services from overload
-
Graceful Degradation:
- Disable non-critical features (recommendations, comments)
- Serve lower quality videos
- Queue non-urgent operations
- Load Shedding: Reject requests with 503 when overloaded
- Pre-warming: If spike is predictable, pre-populate CDN caches
Q: How do you handle millions of concurrent uploads?
A:
- Chunked Uploads: Break into 5MB chunks, upload in parallel
- Upload Service Cluster: Horizontal scaling with load balancer
- Queue-Based Transcoding: Decouple upload from processing
- Priority Queue: Prioritize verified channels, smaller videos
- Backpressure: Slow down uploads if queue is full
- Direct S3 Upload: Generate pre-signed URLs, client uploads directly to S3
11.2 Performance Questions
Q: Video start time is 5 seconds. How to reduce to <2 seconds?
A:
- Reduce Initial Manifest Size: Serve only first 30s of manifest
- Preload First Segment: Embed first segment in HTML (inline)
- Adaptive Initial Quality: Start with 360p, upgrade after buffering
- CDN Edge Optimization: Ensure nearest edge has content
- HTTP/2 Server Push: Push manifest + first segment together
- Reduce DNS Lookup: Use DNS prefetching, HTTP keep-alive
- Optimize Encoding: Use faster codec profiles for first segments
Q: How do you optimize for mobile devices with limited bandwidth?
A:
- Aggressive Quality Downscaling: Start with 240p on slow networks
- Reduce Segment Size: 2-second segments instead of 10-second
- Thumbnail Sprites: Single image with all seek thumbnails
- Data Saver Mode: Lower quality, disable autoplay
- Offline Download: Allow download for offline viewing
- Adaptive Preloading: Reduce preload buffer on mobile
- Video Compression: Use H.265/VP9 for better compression
11.3 Consistency & Reliability Questions
Q: How do you ensure view counts are accurate?
A:
- Challenge: Exact accuracy is expensive at scale
-
Solution: Approximate counting with eventual consistency
- Client sends view event after 30s of watch time
- Event logged to Kafka/Kinesis
- Stream processor (Flink) aggregates events in 5-min windows
- Batch update to Redis counter
- Periodic flush to database (every hour)
- Tolerate 5-10 min delay in count updates
- De-duplication: Use session ID + video ID to prevent double-counting
- Bot Detection: Filter out bot traffic, suspicious IPs
Q: What happens if transcoding service crashes mid-job?
A:
- Job Queue with Retry: Job remains in queue until acknowledged
- Worker Heartbeat: Workers send heartbeat every 30s
- Job Timeout: If no heartbeat for 2 min, job returns to queue
- Max Retries: Retry up to 3 times, then mark as failed
- Checkpoint State: Save transcoding progress every 20%
- Resume from Checkpoint: New worker resumes from last checkpoint
- Dead Letter Queue: Failed jobs go to DLQ for manual investigation
11.4 Data Modeling Questions
Q: Why use both SQL and NoSQL databases?
A:
-
SQL (MySQL/PostgreSQL):
- Structured data with strong relationships
- ACID transactions (e.g., user subscriptions)
- Complex queries (e.g., search, recommendations)
- Examples: Videos, Users, Channels
-
NoSQL (Cassandra/DynamoDB):
- High write throughput (comments, analytics)
- Flexible schema (user-generated content)
- Time-series data (watch history, view counts)
- Scalability (billions of comments)
- Examples: Comments, Watch History, Analytics
Q: How do you handle video deletion while ensuring no orphaned data?
A:
- Soft Delete: Mark video as deleted, don’t remove immediately
-
Background Cleanup Job:
- Delete all resolutions from S3
- Delete thumbnails
- Delete comments (Cassandra)
- Delete analytics data
- Remove from CDN cache
- Remove from search index
- Cascading Delete: Use database foreign keys with ON DELETE CASCADE
- Async Queue: Queue delete operations for background processing
- Audit Log: Keep deletion record for compliance
- Grace Period: 30-day trash period before permanent deletion
11.5 Cost Optimization Questions
Q: Video storage and bandwidth costs are very high. How to optimize?
A:
- Storage Optimization:
- Delete rarely watched videos (after warning user)
- Archive old videos to cheaper cold storage (Glacier)
- De-duplicate identical videos
- Remove redundant resolutions (e.g., skip 1440p)
- Use better compression (H.265, VP9, AV1)
- Bandwidth Optimization:
- Aggressive CDN caching (reduce origin bandwidth)
- Smart preloading (don’t preload if user won’t watch)
- Disable autoplay on mobile
- Lower default quality on slow networks
- Use P2P delivery for live streams (WebRTC mesh)
-
Transcoding Optimization:
- Adaptive transcoding (don’t generate 4K for short videos)
- On-demand transcoding (only transcode when requested)
- Use cheaper GPU instances for encoding
- Batch transcode jobs during off-peak hours
Q: How do you decide which videos to cache on CDN?
A:
- Multi-factor scoring:
- Popularity: View count, trending score
- Recency: Newly uploaded videos
- Geography: Popular in specific regions
- Channel: Verified channels, high subscriber count
- Content Type: Music videos, viral content
-
Cache Tiers:
- Hot Tier (SSD, all edges): Top 1% most popular
- Warm Tier (HDD, major edges): Top 10%
- Cold Tier (origin fetch): Long-tail content
-
Eviction Policy: LRU with weighted scoring
11.6 Real-Time Features Questions
Q: How would you implement live streaming?
A:
- Ingest:
- Streamer uses RTMP/WebRTC to push to ingest server
- Ingest server in nearest region
- Transcoding:
- Real-time transcoding to multiple qualities
- Low-latency encoding (<3s delay)
- Distribution:
- HLS for regular live (10-30s delay acceptable)
- WebRTC for ultra-low latency (<1s delay)
- CDN edge caching of live segments
- Playback:
- Adaptive bitrate streaming
- Live DVR (rewind live stream)
- Chat synchronization
-
Fallback:
- Automatic archive to VOD after stream ends
Q: How do you implement real-time comment updates?
A:
- WebSocket Connection: Persistent connection for real-time updates
-
Pub/Sub System: Redis Pub/Sub or Kafka
- User posts comment -> Publish to topic
- All connected clients subscribed to topic receive update
-
Scaling:
- Multiple WebSocket servers behind load balancer
- Sticky sessions for connection persistence
- Redis for cross-server message passing
- Fallback: HTTP long-polling if WebSocket unavailable
- Optimization: Only send updates for visible comments (top 50)
11.7 Security Questions
Q: How do you prevent unauthorized video access?
A:
- Authentication: JWT tokens for user identity
-
Authorization: Check video privacy settings
- Public: Anyone can watch
- Unlisted: Only with direct link
- Private: Only owner/invited users
- Signed URLs: Time-limited, encrypted video URLs
https://cdn.example.com/video.m3u8?token=xyz&expires=1234567890
- Token Rotation: Short-lived tokens (5-15 min)
- DRM: Encrypted video with Widevine/FairPlay for premium content
- Geo-Blocking: Restrict content by region if required
Q: How do you prevent video piracy?
A:
- DRM Encryption: Widevine, FairPlay, PlayReady
- Watermarking: Visible/invisible watermarks with user ID
- HDCP: Prevent screen recording (hardware-level)
- Forensic Watermarking: Trace leaked videos back to source
- Download Restrictions: Disable right-click, inspect element
- Rate Limiting: Prevent bulk downloading
- Legal: DMCA takedown process, content ID matching
12. Trade-offs & Design Decisions
SQL vs NoSQL for Comments
Decision: Use NoSQL (Cassandra)
- Pro: Better write scalability for high-volume comments
- Pro: Time-ordered retrieval (TIMEUUID)
- Con: Limited query flexibility
- Con: Eventual consistency
HLS vs DASH
Decision: Support both, prefer HLS
- HLS: Wider browser support (Safari, iOS)
- DASH: Open standard, better features
- Solution: Serve HLS to Apple devices, DASH to others
Synchronous vs Asynchronous Transcoding
Decision: Asynchronous with job queue
- Pro: Fast upload response (<5s)
- Pro: Decouple upload from processing
- Con: Video not immediately available
- Mitigation: Show processing status, estimate completion time
CDN vs Self-Hosted Streaming
Decision: Use CDN (CloudFront, Akamai)
- Pro: Global edge caching, low latency
- Pro: DDoS protection, high availability
- Con: Expensive for high traffic
- Mitigation: Aggressive caching, P2P for live streams
Exact vs Approximate View Counting
Decision: Approximate counting with 5-min delay
- Pro: Massive scalability improvement
- Pro: Reduced database write load
- Con: Slight delay in count updates
- Why: Users tolerate small delays for view counts
Summary
This design provides a scalable, performant, and reliable video streaming platform similar to YouTube. Key highlights:
- Scalability: Horizontally scalable services, CDN for global reach
- Performance: Adaptive bitrate streaming, multi-layer caching, <2s start time
- Reliability: Queue-based transcoding, retry mechanisms, graceful degradation
- Cost Efficiency: Aggressive caching, smart preloading, compression
- User Experience: Smooth playback, real-time comments, personalized recommendations
The architecture handles millions of concurrent users, supports live and VOD streaming, and provides a YouTube-like experience with adaptive quality, comments, and recommendations.
13. Accessibility (a11y)
Video Player Keyboard Controls
┌─────────────────────────────────────────────────────────────────────────────┐
│ VIDEO PLAYER KEYBOARD SHORTCUTS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Playback Controls: │
│ ────────────────── │
│ Space / K Play / Pause │
│ J Rewind 10 seconds │
│ L Fast forward 10 seconds │
│ ← / → Seek backward/forward 5 seconds │
│ Home Go to beginning │
│ End Go to end │
│ 0-9 Jump to 0%-90% of video │
│ │
│ Volume Controls: │
│ ──────────────── │
│ M Mute / Unmute │
│ ↑ / ↓ Increase / Decrease volume 5% │
│ │
│ Display Controls: │
│ ───────────────── │
│ F Toggle fullscreen │
│ Escape Exit fullscreen │
│ C Toggle captions │
│ < / > Decrease / Increase playback speed │
│ I Toggle mini-player │
│ T Toggle theater mode │
│ │
│ Navigation: │
│ ─────────── │
│ Tab Navigate between controls │
│ Shift+N Next video in playlist │
│ Shift+P Previous video in playlist │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Accessible Video Player Implementation
// AccessibleVideoPlayer.tsx
const AccessibleVideoPlayer = ({ videoId, captions }: VideoPlayerProps) => {
const videoRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false);
const [captionsEnabled, setCaptionsEnabled] = useState(false);
const announcer = useRef<HTMLDivElement>(null);
// Screen reader announcements
const announce = (message: string) => {
if (announcer.current) {
announcer.current.textContent = message;
// Clear after announcement
setTimeout(() => {
if (announcer.current) announcer.current.textContent = "";
}, 1000);
}
};
// Keyboard handler
const handleKeyDown = (e: KeyboardEvent) => {
const video = videoRef.current;
if (!video) return;
// Don't handle if typing in input
if (e.target instanceof HTMLInputElement) return;
switch (e.key) {
case " ":
case "k":
e.preventDefault();
togglePlay();
break;
case "j":
e.preventDefault();
seekBy(-10);
announce("Rewound 10 seconds");
break;
case "l":
e.preventDefault();
seekBy(10);
announce("Fast forwarded 10 seconds");
break;
case "ArrowLeft":
e.preventDefault();
seekBy(-5);
announce("Rewound 5 seconds");
break;
case "ArrowRight":
e.preventDefault();
seekBy(5);
announce("Fast forwarded 5 seconds");
break;
case "ArrowUp":
e.preventDefault();
adjustVolume(0.05);
break;
case "ArrowDown":
e.preventDefault();
adjustVolume(-0.05);
break;
case "m":
e.preventDefault();
toggleMute();
break;
case "f":
e.preventDefault();
toggleFullscreen();
break;
case "c":
e.preventDefault();
toggleCaptions();
break;
default:
// Number keys 0-9 for percentage seek
if (e.key >= "0" && e.key <= "9") {
e.preventDefault();
const percent = parseInt(e.key) * 10;
seekToPercent(percent);
announce(`Jumped to ${percent}%`);
}
}
};
const togglePlay = () => {
const video = videoRef.current;
if (!video) return;
if (video.paused) {
video.play();
setIsPlaying(true);
announce("Playing");
} else {
video.pause();
setIsPlaying(false);
announce("Paused");
}
};
const adjustVolume = (delta: number) => {
const video = videoRef.current;
if (!video) return;
const newVolume = Math.max(0, Math.min(1, video.volume + delta));
video.volume = newVolume;
setVolume(newVolume);
announce(`Volume ${Math.round(newVolume * 100)}%`);
};
const toggleCaptions = () => {
setCaptionsEnabled(!captionsEnabled);
announce(captionsEnabled ? "Captions off" : "Captions on");
};
return (
<div
className="video-player-container"
role="application"
aria-label="Video player"
onKeyDown={handleKeyDown}
tabIndex={0}
>
{/* Screen reader announcements */}
<div
ref={announcer}
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
/>
<video
ref={videoRef}
aria-label={`Video: ${title}`}
onTimeUpdate={() => setCurrentTime(videoRef.current?.currentTime || 0)}
onLoadedMetadata={() => setDuration(videoRef.current?.duration || 0)}
>
{captionsEnabled &&
captions.map((caption) => (
<track
key={caption.language}
kind="captions"
src={caption.url}
srcLang={caption.language}
label={caption.label}
default={caption.isDefault}
/>
))}
</video>
{/* Accessible controls */}
<div
className="video-controls"
role="toolbar"
aria-label="Video controls"
>
<button
aria-label={isPlaying ? "Pause" : "Play"}
aria-pressed={isPlaying}
onClick={togglePlay}
>
{isPlaying ? <PauseIcon /> : <PlayIcon />}
</button>
<div className="timeline-container">
<input
type="range"
aria-label="Video timeline"
aria-valuemin={0}
aria-valuemax={duration}
aria-valuenow={currentTime}
aria-valuetext={formatTime(currentTime)}
value={currentTime}
max={duration}
onChange={(e) => seekTo(parseFloat(e.target.value))}
/>
<span className="sr-only">
{formatTime(currentTime)} of {formatTime(duration)}
</span>
</div>
<button
aria-label={isMuted ? "Unmute" : "Mute"}
aria-pressed={isMuted}
onClick={toggleMute}
>
{isMuted ? <MuteIcon /> : <VolumeIcon />}
</button>
<input
type="range"
aria-label="Volume"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(volume * 100)}
value={volume * 100}
max={100}
onChange={(e) => setVolume(parseFloat(e.target.value) / 100)}
/>
<button
aria-label={
captionsEnabled ? "Turn off captions" : "Turn on captions"
}
aria-pressed={captionsEnabled}
onClick={toggleCaptions}
>
<CaptionsIcon />
</button>
<button aria-label="Enter fullscreen" onClick={toggleFullscreen}>
<FullscreenIcon />
</button>
</div>
</div>
);
};
Captions & Subtitles
// CaptionManager.tsx
interface Caption {
startTime: number;
endTime: number;
text: string;
}
const CaptionManager = ({
captions,
currentTime,
enabled,
style,
}: CaptionManagerProps) => {
const [activeCaption, setActiveCaption] = useState<Caption | null>(null);
useEffect(() => {
if (!enabled) {
setActiveCaption(null);
return;
}
const caption = captions.find(
(c) => currentTime >= c.startTime && currentTime <= c.endTime
);
setActiveCaption(caption || null);
}, [currentTime, captions, enabled]);
if (!activeCaption) return null;
return (
<div
className="caption-container"
role="region"
aria-label="Video captions"
aria-live="off" // Don't announce each caption
style={{
backgroundColor: style.backgroundColor,
color: style.textColor,
fontSize: style.fontSize,
fontFamily: style.fontFamily,
}}
>
{activeCaption.text}
</div>
);
};
// Caption settings panel
const CaptionSettings = ({ onStyleChange }: CaptionSettingsProps) => {
return (
<div
role="dialog"
aria-label="Caption settings"
className="caption-settings"
>
<h3 id="caption-settings-title">Caption Settings</h3>
<div role="group" aria-labelledby="font-size-label">
<label id="font-size-label">Font Size</label>
<select
aria-describedby="font-size-label"
onChange={(e) => onStyleChange("fontSize", e.target.value)}
>
<option value="75%">75%</option>
<option value="100%">100% (Default)</option>
<option value="150%">150%</option>
<option value="200%">200%</option>
</select>
</div>
<div role="group" aria-labelledby="font-family-label">
<label id="font-family-label">Font Family</label>
<select onChange={(e) => onStyleChange("fontFamily", e.target.value)}>
<option value="sans-serif">Sans-serif</option>
<option value="serif">Serif</option>
<option value="monospace">Monospace</option>
</select>
</div>
<div role="group" aria-labelledby="bg-color-label">
<label id="bg-color-label">Background</label>
<select
onChange={(e) => onStyleChange("backgroundColor", e.target.value)}
>
<option value="rgba(0,0,0,0.75)">Black (Default)</option>
<option value="rgba(255,255,255,0.75)">White</option>
<option value="transparent">Transparent</option>
</select>
</div>
</div>
);
};
Focus Management
// useFocusTrap.ts - Trap focus within video player settings
const useFocusTrap = (
isActive: boolean,
containerRef: RefObject<HTMLElement>
) => {
useEffect(() => {
if (!isActive || !containerRef.current) return;
const container = containerRef.current;
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[
focusableElements.length - 1
] as HTMLElement;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== "Tab") return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
container.addEventListener("keydown", handleKeyDown);
firstElement?.focus();
return () => {
container.removeEventListener("keydown", handleKeyDown);
};
}, [isActive, containerRef]);
};
// VideoSettingsDialog.tsx
const VideoSettingsDialog = ({ isOpen, onClose }: SettingsDialogProps) => {
const dialogRef = useRef<HTMLDivElement>(null);
const previouslyFocusedRef = useRef<HTMLElement | null>(null);
useFocusTrap(isOpen, dialogRef);
useEffect(() => {
if (isOpen) {
// Store previously focused element
previouslyFocusedRef.current = document.activeElement as HTMLElement;
} else {
// Restore focus when closing
previouslyFocusedRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="settings-title"
className="settings-dialog"
>
<h2 id="settings-title">Video Settings</h2>
{/* Settings content */}
<button onClick={onClose} aria-label="Close settings">
Close
</button>
</div>
);
};
Screen Reader Optimizations
┌─────────────────────────────────────────────────────────────────────────────┐
│ SCREEN READER ANNOUNCEMENTS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Playback Events: │
│ ──────────────── │
│ • "Playing" / "Paused" │
│ • "Video ended" │
│ • "Buffering..." / "Playback resumed" │
│ • "Rewound 10 seconds" │
│ • "Fast forwarded 10 seconds" │
│ │
│ Quality Changes: │
│ ──────────────── │
│ • "Quality changed to 1080p" │
│ • "Auto quality: switching to 720p" │
│ │
│ Volume: │
│ ─────── │
│ • "Volume 50%" │
│ • "Muted" / "Unmuted" │
│ │
│ Captions: │
│ ───────── │
│ • "Captions on: English" │
│ • "Captions off" │
│ │
│ Navigation: │
│ ─────────── │
│ • "Jumped to 50%" │
│ • "Now playing: [Video Title]" │
│ • "Entered fullscreen" / "Exited fullscreen" │
│ │
│ Implementation: │
│ ─────────────── │
│ │
│ {announcement} │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Video Grid Accessibility
// AccessibleVideoGrid.tsx
const AccessibleVideoGrid = ({ videos }: VideoGridProps) => {
return (
<section aria-label="Video recommendations">
<h2 id="recommendations-heading">Recommended Videos</h2>
<ul
role="list"
aria-labelledby="recommendations-heading"
className="video-grid"
>
{videos.map((video, index) => (
<li key={video.id}>
<article
className="video-card"
aria-labelledby={`video-title-${video.id}`}
>
<a
href={`/watch?v=${video.id}`}
aria-describedby={`video-meta-${video.id}`}
>
<img
src={video.thumbnailUrl}
alt="" // Decorative, title provides context
loading="lazy"
/>
<div className="video-duration" aria-hidden="true">
{formatDuration(video.duration)}
</div>
</a>
<div className="video-info">
<h3 id={`video-title-${video.id}`}>
<a href={`/watch?v=${video.id}`}>{video.title}</a>
</h3>
<p id={`video-meta-${video.id}`} className="video-meta">
<span>{video.channelName}</span>
<span aria-label={`${video.views} views`}>
{formatViews(video.views)} views
</span>
<span aria-label={`uploaded ${video.uploadedAt}`}>
{formatRelativeTime(video.uploadedAt)}
</span>
<span className="sr-only">
Duration: {formatDurationAccessible(video.duration)}
</span>
</p>
</div>
</article>
</li>
))}
</ul>
</section>
);
};
// Accessible duration formatting
const formatDurationAccessible = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
const parts = [];
if (hours > 0) parts.push(`${hours} hour${hours > 1 ? "s" : ""}`);
if (minutes > 0) parts.push(`${minutes} minute${minutes > 1 ? "s" : ""}`);
if (secs > 0) parts.push(`${secs} second${secs > 1 ? "s" : ""}`);
return parts.join(" ");
};
14. Security & Content Protection
DRM (Digital Rights Management) Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ DRM ENCRYPTION FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Video Upload → Encode → Encrypt → Store │
│ │
│ ┌──────────┐ │
│ │ Raw │ │
│ │ Video │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ENCODING & ENCRYPTION │ │
│ │ │ │
│ │ 1. Transcode to multiple resolutions │ │
│ │ 2. Generate encryption keys (per video) │ │
│ │ 3. Encrypt each segment with AES-128-CTR │ │
│ │ 4. Create DRM licenses: │ │
│ │ • Widevine (Chrome, Android) │ │
│ │ • FairPlay (Safari, iOS) │ │
│ │ • PlayReady (Edge, Windows) │ │
│ │ 5. Store encrypted segments + license server URLs │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Encrypted│ │ License │ │ Key │ │
│ │ Segments │ │ Server │ │ Server │ │
│ │ (S3) │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Playback Flow: │
│ ─────────────── │
│ 1. Client requests manifest (encrypted video reference) │
│ 2. Client requests license from License Server │
│ 3. License Server validates user subscription/rental │
│ 4. License Server returns decryption key │
│ 5. Client CDM (Content Decryption Module) decrypts video │
│ 6. Decrypted video played in protected path │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Widevine DRM Implementation
// DRMPlayer.tsx - Multi-DRM Video Player
const DRMPlayer = ({ videoId, manifestUrl }: DRMPlayerProps) => {
const videoRef = useRef<HTMLVideoElement>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const initDRM = async () => {
const video = videoRef.current;
if (!video) return;
// Detect DRM support
const drmConfig = await detectDRMSupport();
if (!drmConfig) {
setError("DRM not supported on this browser");
return;
}
try {
// Initialize Shaka Player with DRM
const shaka = await import("shaka-player");
shaka.polyfill.installAll();
const player = new shaka.Player(video);
player.configure({
drm: {
servers: {
"com.widevine.alpha": `${API_URL}/drm/widevine/license?videoId=${videoId}`,
"com.apple.fps.1_0": `${API_URL}/drm/fairplay/license?videoId=${videoId}`,
"com.microsoft.playready": `${API_URL}/drm/playready/license?videoId=${videoId}`,
},
},
});
// FairPlay requires certificate
if (drmConfig.keySystem === "com.apple.fps.1_0") {
const cert = await fetchFairPlayCertificate();
player.configure(
"drm.advanced.com\.apple\.fps\.1_0.serverCertificate",
cert
);
}
await player.load(manifestUrl);
} catch (err) {
console.error("DRM initialization failed:", err);
setError("Failed to load protected content");
}
};
initDRM();
}, [videoId, manifestUrl]);
return (
<div className="drm-player">
{error && (
<div className="drm-error" role="alert">
<p>{error}</p>
<p>Try using Chrome, Safari, or Edge for protected content.</p>
</div>
)}
<video ref={videoRef} controls />
</div>
);
};
// Detect which DRM system is supported
const detectDRMSupport = async (): Promise<DRMConfig | null> => {
const keySystems = [
{ keySystem: "com.widevine.alpha", name: "Widevine" },
{ keySystem: "com.apple.fps.1_0", name: "FairPlay" },
{ keySystem: "com.microsoft.playready", name: "PlayReady" },
];
for (const config of keySystems) {
try {
const result = await navigator.requestMediaKeySystemAccess(
config.keySystem,
[
{
initDataTypes: ["cenc"],
videoCapabilities: [
{
contentType: 'video/mp4; codecs="avc1.42E01E"',
},
],
},
]
);
if (result) {
return config;
}
} catch (e) {
// This DRM not supported, try next
}
}
return null;
};
Signed URL Implementation
// signedUrl.ts - Generate time-limited signed URLs
interface SignedUrlParams {
videoId: string;
userId: string;
expiresIn: number; // seconds
ipAddress?: string;
}
// Server-side: Generate signed URL
const generateSignedUrl = (params: SignedUrlParams): string => {
const expires = Math.floor(Date.now() / 1000) + params.expiresIn;
const dataToSign = [
params.videoId,
params.userId,
expires.toString(),
params.ipAddress || "",
].join(":");
const signature = crypto
.createHmac("sha256", process.env.URL_SIGNING_SECRET!)
.update(dataToSign)
.digest("hex");
const queryParams = new URLSearchParams({
videoId: params.videoId,
expires: expires.toString(),
signature,
...(params.ipAddress && { ip: params.ipAddress }),
});
return `${CDN_BASE_URL}/videos/${params.videoId}/manifest.m3u8?${queryParams}`;
};
// Client-side: Request signed URL before playback
const getVideoUrl = async (videoId: string): Promise<string> => {
const response = await fetch(`/api/v1/videos/${videoId}/play`, {
headers: {
Authorization: `Bearer ${getAuthToken()}`,
},
});
const { signedUrl, expiresAt } = await response.json();
// Store expiration for refresh
videoUrlCache.set(videoId, { url: signedUrl, expiresAt });
return signedUrl;
};
// Auto-refresh signed URL before expiration
const useSignedUrl = (videoId: string) => {
const [signedUrl, setSignedUrl] = useState<string | null>(null);
const refreshTimer = useRef<NodeJS.Timeout>();
useEffect(() => {
const fetchAndRefresh = async () => {
const response = await getVideoUrl(videoId);
setSignedUrl(response.url);
// Refresh 1 minute before expiration
const refreshIn = response.expiresAt - Date.now() - 60000;
refreshTimer.current = setTimeout(fetchAndRefresh, refreshIn);
};
fetchAndRefresh();
return () => {
if (refreshTimer.current) {
clearTimeout(refreshTimer.current);
}
};
}, [videoId]);
return signedUrl;
};
Content ID & Copyright Detection
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONTENT ID MATCHING FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Upload Flow with Content ID Check: │
│ ────────────────────────────────── │
│ │
│ User Upload │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Extract Audio/ │ │
│ │ Video Fingerprint│ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Compare Against │ │
│ │ Reference DB │ │
│ │ (100M+ tracks) │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ │ │
│ Match? No Match │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Check │ │ Publish │ │
│ │ Policy │ │ Video │ │
│ └────┬────┘ └─────────┘ │
│ │ │
│ ├─────────────┬─────────────┬─────────────┐ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ Block Monetize Track Only Allow │
│ (Takedown) (Ads for owner) (Analytics) (Licensed) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Age-Restricted Content UI
// AgeVerification.tsx
const AgeVerification = ({ videoId, onVerified }: AgeVerificationProps) => {
const [birthDate, setBirthDate] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const verifyAge = () => {
const birth = new Date(birthDate);
const today = new Date();
const age = Math.floor(
(today.getTime() - birth.getTime()) / (365.25 * 24 * 60 * 60 * 1000)
);
if (age >= 18) {
// Store verification in session
sessionStorage.setItem("ageVerified", "true");
onVerified();
} else {
setError("You must be 18 or older to view this content.");
}
};
return (
<div className="age-verification" role="dialog" aria-labelledby="age-title">
<div className="age-content">
<WarningIcon />
<h2 id="age-title">Age-Restricted Content</h2>
<p>
This video may be inappropriate for some users. Please confirm your
age to continue.
</p>
<div className="age-form">
<label htmlFor="birthdate">Date of Birth</label>
<input
id="birthdate"
type="date"
value={birthDate}
onChange={(e) => setBirthDate(e.target.value)}
max={new Date().toISOString().split("T")[0]}
aria-describedby={error ? "age-error" : undefined}
/>
{error && (
<p id="age-error" className="error" role="alert">
{error}
</p>
)}
<button onClick={verifyAge} disabled={!birthDate}>
Confirm Age
</button>
</div>
<p className="privacy-notice">
We don't store your date of birth.
Learn more