Course Content
Mobile System Design Interview
11 sections · 23 lessons
YouTube: the playback session, offline downloads and follow-ups
The previous lesson kept a picture on screen. This one designs the app around it: the layered diagram again, with one addition no other case study needs — playback is a session that outlives the screen showing it.
After the architecture, the lesson turns to downloads, which are the whole offline story for a video app, and then to the follow-ups, where interviewers check that you apply the same principles — batching, bounded prefetch, honest labelling — consistently.
The player abstraction
Wrap the platform player behind your own interface:
interface Player { load(source: MediaSource, startAtMs: Long) play(); pause(); seekTo(ms) setQualityOverride(q: Quality?) val state: Stream<PlaybackState> // idle|buffering|playing|paused|ended|error val position: Stream<Long>}Three reasons, and it is worth giving all three because the first alone sounds like ceremony: the two platforms' players have different application programming interfaces, so this is where the cross-platform difference is contained; playback becomes testable with a fake implementation; and swapping the underlying player — which teams do — becomes one file rather than a hundred.
What the abstraction must not do is hide the platform player's buffering and quality controls behind a lowest-common-denominator interface. Expose them; the adaptive logic needs them.
Playback state that survives rotation and backgrounding
Playback position must not live in the screen. Rotation may destroy and rebuild the view, and backgrounding may destroy the process.
Three tiers, matching Step 4: data flow and state:
| State | Lives in | Survives |
|---|---|---|
| Controls visible, scrubbing in progress | The view | Nothing |
| Current position, quality, playing/paused | The playback session (outside the screen) | Rotation, navigation |
| Last watched position per video | The database, written every few seconds | Process death, and syncs across devices |
Write the position on a coalesced timer — every 5–10 seconds and on pause, background, and end — rather than on every frame. Same pattern as Throttling and coalescing updates.
The session model
Picture-in-picture and background audio both require that playback continue while your screen does not exist. So playback is owned by a component with a lifetime independent of any screen: a playback session that holds the player, the current item, and the queue, and that screens attach to and detach from.
The session also owns everything the operating system needs to show playback outside your app: the media metadata for lock-screen and system controls, transport commands from headphones and car systems, and audio focus — pausing when a call arrives, ducking for a navigation prompt, and deciding whether to resume afterwards.
Offline: the session and player are unchanged — the media source is a local file rather than a manifest URL. Building the abstraction around a MediaSource that can be either is what makes offline playback the same code path rather than a second implementation.
Offline downloads
That MediaSource has to come from somewhere. A download is not a cached stream. It is a deliberate act by the user, with a quality choice, a storage cost, and — for licensed content — an expiry.
Quality is chosen at download time
Streaming adapts; a download cannot. So the user picks, and the choice is a data-and-storage decision that should be presented as one:
Download "Ocean Documentary" (52 min) ○ 360p ~275 MB Standard ● 720p ~950 MB High ← default on Wi-Fi ○ 1080p ~1.7 GB Very high 3.2 GB free on this deviceShowing the size and the free space in the same panel is what turns a support ticket into a decision the user made. A default that matches the connection — lower on cellular — and a remembered preference cover most cases.
The transfer, under background limits
Same mechanism as Upload and download in the Google Drive app, and the numbers make it unavoidable: a 950 MB download at 5 Mbps takes about 25 minutes. That is far beyond any execution window your app is given, so it goes to the platform's transfer service or a constrained deferred job.
Download by segment, since the content is already segmented, which gives resumability for free: on interruption the client knows exactly which segments it holds and requests the rest. Persist progress in the database so it survives process death, and coalesce progress writes.
Constraints to declare on the job: unmetered network by default, and — for large downloads — prefer charging, since 25 minutes of sustained radio and disk activity is a real battery cost. Surface an explicit "waiting for Wi-Fi" state so a queued download never looks broken.
Licensed content, encryption, and expiry
For licensed content the segments are stored encrypted and a licence — the decryption key plus its rules — is fetched separately and held in the platform's secure store.
The property that surprises people: licences expire. A typical rule is a validity window from download and a shorter one from first playback. So a user who downloads a film and flies a week later may find it will not play, and renewing needs a network they do not have.
The design responsibility is entirely client-side and mostly about honesty: show the expiry on the download, warn before it lapses while there is still a network, offer renewal in one tap, and never present an expired download as playable. Do not silently delete expired content either — the user chose to download it, and a clear "expired, tap to renew" is better than an item that vanished.
Storage management
Downloads are pinned content by definition and are never evicted automatically. Provide a downloads screen listing size per item, total used, and free space; a delete action; and an optional "delete after watching". Exclude downloaded media from cloud backup, or the user's backup balloons by gigabytes.
Offline: downloads are the offline story for this app. The measure of the design is whether a user who prepared before a flight has everything they need, and whether one who did not gets a clear explanation rather than a spinner.
Follow-ups
Cellular data warnings and a data-saver mode
Using the numbers from the first lesson of this case study: warn before any download over a threshold on cellular, cap streaming quality on metered connections by default, and offer a data-saver mode that caps at 360p and disables prefetch and autoplay entirely.
Give the user a data-used figure in the app. A warning about data costs is more persuasive when the app can show what it has actually spent, and it is a genuine trust feature.
Playback analytics without draining the battery
Streaming products need detailed telemetry — start time, rebuffer events and durations, quality switches, bitrate over time, watch progress, errors. Sending each event as it happens would mean dozens of requests per video and a radio kept busy for no user benefit.
The pattern is the one from The mobile constraints, quantified and Observability and rollout: buffer events in memory, persist them to a local queue so they survive process death, and upload in batches — on a timer, when the buffer reaches a size, at session end, and opportunistically when the radio is already awake for something else. One upload of two hundred events costs far less than two hundred uploads. Sample high-frequency events rather than recording every one, and never let analytics compete with segment fetches for bandwidth during playback.
Preloading the next video in a queue
For an autoplay queue or a series, prefetch the manifest and first segments of the next item during the last ~30 seconds of the current one. Bandwidth is usually spare then, because the current video's buffer is full. Bound it exactly as in the buffer design — unmetered only, first segments only, cancelled if the user leaves.
Live streams and their much smaller buffer
Live inverts the buffer trade. A 30-second buffer means the viewer is 30 seconds behind, which is unacceptable for anything people discuss in real time. So live runs a buffer of a few seconds, with shorter segments, which means far less protection from network variation and a much higher rebuffer risk.
Two consequences worth naming: quality adaptation must be more conservative, because there is no buffer to absorb a mistake; and the client needs a catch-up strategy after a stall — either skip forward to the live edge or play slightly faster to close the gap, which is less disruptive and is what most players do.
Accessibility for captions and controls
Captions are not a feature to bolt on. Support the platform's caption styling preferences — font size, colour, background opacity — rather than hard-coding an appearance, since users who need captions have often configured those settings deliberately. Support multiple caption tracks and audio description tracks where the content has them.
For controls: every button needs a label and a tap target of at least the platform minimum (around 44–48 points), the scrubber needs an accessible adjustable action so it can be moved without dragging, and playback state changes should be announced. Honour reduce-motion for autoplay, and never let a control auto-hide so fast that a user with a motor impairment cannot reach it — a longer timeout when a screen reader or switch control is active is a small change with a large effect.