Definition
- Async processing – Transcoding with FFmpeg is CPU-heavy; accept the upload instantly and process via a queue and background workers.
- Multiple renditions – Encode at several bitrates (240p, 480p, 720p, 1080p) split into short segments.
- Adaptive streaming – HLS/DASH manifest lets the player pick the quality that matches current bandwidth, switching segment by segment.
- CDN edge – Encoded segments are cached worldwide; viewers fetch from the nearest edge.
- Object storage – Raw and encoded files in S3/GCS; the database stores only metadata.
- View tracking – Aggregate counts asynchronously; per-play DB increments do not scale.
National Bank of India records training videos for staff and KYC videos for customers like Rajesh. You cannot make them wait while the video is converted into every quality. The upload is accepted instantly, dropped into an in-tray, and workers convert it into small, medium, and large versions. When Rajesh watches on a slow train, the player plays the small version; on office WiFi it plays HD. The video is served from a nearby branch (CDN edge), not head office.
Understanding the Concepts
Why must uploads be processed asynchronously?
Transcoding a video into multiple qualities is CPU-heavy and slow. If done synchronously, the uploader waits minutes and the request times out.
package com.bank.video.upload;
public class VideoUploadService {
private final ObjectStorageClient storage;
private final MessageQueue queue;
private final VideoMetadataRepository metadataRepo;
public VideoUploadService(ObjectStorageClient storage, MessageQueue queue,
VideoMetadataRepository metadataRepo) {
this.storage = storage;
this.queue = queue;
this.metadataRepo = metadataRepo;
}
public String uploadVideo(String userId, String filename, byte[] rawContent) {
// Step 1: Store raw master in object storage
String rawKey = "raw/" + userId + "/" + filename;
storage.put(rawKey, rawContent);
// Step 2: Save metadata with status PROCESSING
String videoId = generateId();
metadataRepo.save(videoId, userId, rawKey, "PROCESSING");
// Step 3: Enqueue processing job
queue.publish("video-processing", new TranscodeJob(videoId, rawKey));
// Return immediately: uploader does not wait for transcoding
return videoId;
}
private String generateId() {
return "VID-" + System.currentTimeMillis();
}
}
What does the processing pipeline look like?
Raw master goes through FFmpeg to produce multiple renditions, each split into segments, plus thumbnails and an HLS manifest.
package com.bank.video.worker;
public class TranscodeWorker {
public void process(TranscodeJob job) {
// Step 1: Download raw from storage
byte[] raw = storage.get(job.getRawKey());
// Step 2: Transcode to multiple renditions
transcode(raw, "240p", job.getVideoId());
transcode(raw, "480p", job.getVideoId());
transcode(raw, "720p", job.getVideoId());
transcode(raw, "1080p", job.getVideoId());
// Step 3: Generate thumbnails
generateThumbnails(raw, job.getVideoId());
// Step 4: Generate HLS manifest listing all renditions
generateManifest(job.getVideoId());
// Step 5: Update status to PUBLISHED
metadataRepo.updateStatus(job.getVideoId(), "PUBLISHED");
}
private void transcode(byte[] raw, String quality, String videoId) {
// FFmpeg: ffmpeg -i input.mp4 -vf scale=-2:720 -hls_time 4 output_720p.m3u8
System.out.println("Transcoded " + videoId + " to " + quality);
}
private void generateThumbnails(byte[] raw, String videoId) { }
private void generateManifest(String videoId) { }
}
How does adaptive bitrate streaming work?
The player downloads the manifest, continuously measures bandwidth, and picks the highest quality it can download in time.
package com.bank.video.streaming;
public class AdaptiveBitrateExplanation {
// HLS manifest (master.m3u8) lists renditions:
// #EXT-X-STREAM-INF:BANDWIDTH=500000,RESOLUTION=426x240
// 240p/index.m3u8
// #EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720
// 720p/index.m3u8
// #EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1920x1080
// 1080p/index.m3u8
//
// Player behavior:
// - Measures available bandwidth: 2 Mbps
// - Picks 720p (needs 1.5 Mbps) because 1080p (4 Mbps) would buffer
// - If bandwidth drops to 600 Kbps, switches DOWN to 240p
// - If bandwidth improves, switches UP to 1080p
// - Switches happen per segment (4 seconds), no rebuffering
}
Why serve from a CDN edge?
Video is huge and heavily watched. Serving every segment from origin saturates it. A CDN caches segments at edge locations worldwide.
package com.bank.video.delivery;
public class CdnDelivery {
// Without CDN:
// Viewer in Mumbai -> S3 in US-East -> 200ms latency, origin saturated
//
// With CDN:
// Viewer in Mumbai -> CDN edge in Mumbai -> 5ms latency, origin offloaded
// First viewer triggers cache fill from origin
// All subsequent viewers get cached copy from edge
// Popular videos: 99%+ served from edge
}
Where do you store raw and encoded files?
Object storage (S3/GCS) is cheap, durable, and infinite. The database holds only metadata.
package com.bank.video.model;
public class VideoMetadata {
private String videoId;
private String title;
private String ownerId;
private String rawStorageKey; // s3://videos-raw/VID-123/input.mp4
private String hlsManifestKey; // s3://videos-encoded/VID-123/master.m3u8
private String status; // PROCESSING, PUBLISHED, FAILED
private long durationSeconds;
private long viewCount;
// Database stores pointers, NOT the video bytes
}
How do you track views and analytics?
Never increment a DB row per play. Buffer and aggregate asynchronously.
package com.bank.video.analytics;
public class ViewTracker {
public void trackPlaybackEvent(String videoId, String eventType) {
// Events: START, PAUSE, SEEK, QUALITY_SWITCH, COMPLETE
// Emit to Kafka -> analytics pipeline
// View counts: buffered in Redis, flushed to DB periodically
// NOT: UPDATE videos SET view_count = view_count + 1 (does not scale)
}
}
What about retries and failure handling?
Each pipeline stage is retryable. Failed transcodes go to a DLQ for inspection.
package com.bank.video.worker;
public class TranscodeRetryHandler {
public void onFailure(TranscodeJob job, Exception e, int attempt) {
if (attempt < 3) {
queue.publishWithDelay("video-processing", job, attempt * 30_000L);
} else {
deadLetterQueue.publish(job);
metadataRepo.updateStatus(job.getVideoId(), "FAILED");
}
}
}
What are common interview follow-up questions?
How do you handle live streaming vs on-demand? How do you implement DRM (digital rights management)? How would you add subtitles and multiple audio tracks? How do you monitor quality of experience (rebuffering, startup time)? How do you lifecycle old content to cheaper storage?
Visual Overview
┌──────────────────────────────────────────────────────────┐ │ VIDEO STREAMING PLATFORM │ ├──────────────────────────────────────────────────────────┤ │ │ │ UPLOAD PATH (write-heavy, async): │ │ │ │ Client ──upload──▶ API ──raw──▶ S3 (raw master) │ │ │ │ │ enqueue │ │ │ │ │ ▼ │ │ ┌────────────┐ │ │ │ Queue │ │ │ └─────┬──────┘ │ │ │ │ │ ┌─────▼──────────────┐ │ │ │ Transcode Workers │ │ │ │ FFmpeg → 240p,480p │ │ │ │ 720p,1080p + HLS │ │ │ │ + thumbnails │ │ │ └─────┬──────────────┘ │ │ │ │ │ S3 (encoded segments + manifest) │ │ │ │ │ status = PUBLISHED │ │ │ │ PLAYBACK PATH (read-heavy, CDN): │ │ │ │ Viewer ──GET manifest──▶ CDN Edge ──cache miss──▶ S3 │ │ Viewer ──GET segments──▶ CDN Edge (cached, < 5ms) │ │ Player picks quality by bandwidth (adaptive) │ │ │ │ ANALYTICS: events → Kafka → pipeline (async) │ └──────────────────────────────────────────────────────────┘
Key Takeaway
A video streaming platform is two very different systems joined by object storage: a write-heavy async processing pipeline and a read-heavy CDN delivery system. At National Bank of India, Pankaj's early attempt to transcode synchronously caused uploads to time out and the server to melt under concurrent video submissions for KYC verification.
The fix was decoupling: the upload endpoint accepts the raw file (via presigned URL), records metadata with status PROCESSING, and drops a job on a queue. Background workers transcode with FFmpeg into multiple renditions (240p through 1080p), generate thumbnails and an HLS manifest, store everything in S3, and flip the status to PUBLISHED. The viewer never waits on processing.
Playback uses adaptive bitrate streaming: the player downloads the manifest, measures bandwidth, and picks the highest quality it can download per segment, smoothly dropping quality on a weak connection instead of freezing. Segments are served from CDN edges worldwide, hitting the origin only on cache miss. View counts are aggregated asynchronously because per-play DB increments cannot scale.
Remember one sentence: Accept uploads instantly and transcode asynchronously via queue workers into multiple HLS renditions stored in S3, serve segments from a CDN edge for global low latency, and let the player adapt quality to bandwidth segment by segment.
---