Functional Scope (In-Scope)
- PlayState Transition Engines: Manage player states (IDLE, PLAYING, PAUSED, BUFFERING) using a clean State design pattern interface.
- Listen History Tracking: Save user playback progress for each episode to enable resuming where they left off.
- Variable Playback Speed Controllers: Support custom playback speed settings (0.5×–3×) dynamically.
- Offline Queue Managers: Queue and download episodes in the background using parallel multi-threaded worker pools.
Explicit Boundaries (Out-of-Scope)
- No Real-World Audio Decoders/Codecs: Bypasses live MP3 decoding, hardware buffer tuning, and audio hardware integrations.
- No Direct Podcast RSS Parsing: Bypasses real XML/RSS feed parsing from external podcast hosts.
Clean reference designs demonstrating player state machines in Java and Python:
// ─── JAVA BLUEPRINT ──────────────────────────────────────────────────────────
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
enum PlayerState { IDLE, PLAYING, PAUSED, BUFFERING }
enum DownloadStatus { NOT_DOWNLOADED, DOWNLOADING, DOWNLOADED, FAILED }
// Episode model with offline status tracking
class Episode {
private final String id;
private final String title;
private final String audioUrl;
private final int durationSec;
private DownloadStatus downloadStatus = DownloadStatus.NOT_DOWNLOADED;
private final ReentrantLock lock = new ReentrantLock();
public Episode(String id, String title, String audioUrl, int durationSec) {
this.id = id;
this.title = title;
this.audioUrl = audioUrl;
this.durationSec = durationSec;
}
public String getId() { return id; }
public String getTitle() { return title; }
public String getAudioUrl() { return audioUrl; }
public int getDurationSec() { return durationSec; }
public DownloadStatus getDownloadStatus() {
lock.lock();
try {
return downloadStatus;
} finally {
lock.unlock();
}
}
public void setDownloadStatus(DownloadStatus status) {
lock.lock();
try {
this.downloadStatus = status;
} finally {
lock.unlock();
}
}
}
// Podcast representing collections of episodes
class Podcast {
private final String id;
private final String title;
private final String author;
private final List<Episode> episodes = new CopyOnWriteArrayList<>();
public Podcast(String id, String title, String author) {
this.id = id;
this.title = title;
this.author = author;
}
public void addEpisode(Episode ep) {
episodes.add(ep);
}
public String getId() { return id; }
public String getTitle() { return title; }
public String getAuthor() { return author; }
public List<Episode> getEpisodes() { return episodes; }
}
// Track progress of episodes listened by user
class PlaybackProgress {
private final String episodeId;
private int lastPositionSec;
private boolean completed;
private final long lastUpdated;
public PlaybackProgress(String episodeId, int lastPositionSec, boolean completed) {
this.episodeId = episodeId;
this.lastPositionSec = lastPositionSec;
this.completed = completed;
this.lastUpdated = System.currentTimeMillis();
}
public synchronized void updatePosition(int pos, int durationSec) {
this.lastPositionSec = pos;
if ((double) pos / durationSec >= 0.90) {
this.completed = true;
}
}
public String getEpisodeId() { return episodeId; }
public synchronized int getLastPositionSec() { return lastPositionSec; }
public synchronized boolean isCompleted() { return completed; }
}
// Thread-safe download manager utilizing a background executor
class DownloadManager {
private final ExecutorService executor = Executors.newFixedThreadPool(2);
public void downloadEpisode(Episode episode) {
episode.setDownloadStatus(DownloadStatus.DOWNLOADING);
System.out.println("[DOWNLOAD] Starting background download: " + episode.getTitle());
executor.submit(() -> {
try {
// Simulate network latency download
Thread.sleep(1000);
episode.setDownloadStatus(DownloadStatus.DOWNLOADED);
System.out.println("[DOWNLOAD] Successfully downloaded episode offline: " + episode.getTitle());
} catch (InterruptedException e) {
episode.setDownloadStatus(DownloadStatus.FAILED);
Thread.currentThread().interrupt();
}
});
}
public void shutdown() {
executor.shutdown();
}
}
// State Pattern: Decoupling player states and commands
interface PlayerStateController {
void play(PlayerContext player, Episode ep, int position);
void pause(PlayerContext player);
void seek(PlayerContext player, int seconds);
}
class IdleState implements PlayerStateController {
@Override
public void play(PlayerContext player, Episode ep, int position) {
player.setCurrentEpisode(ep);
player.setCurrentPositionSec(position);
player.setState(new PlayingState());
System.out.println("[PLAYER] Transition to PLAYING: " + ep.getTitle() + " from " + position + "s");
}
@Override public void pause(PlayerContext player) { System.out.println("[PLAYER] Cannot pause. Currently idle."); }
@Override public void seek(PlayerContext player, int seconds) { System.out.println("[PLAYER] Cannot seek. Currently idle."); }
}
class PlayingState implements PlayerStateController {
@Override
public void play(PlayerContext player, Episode ep, int position) {
System.out.println("[PLAYER] Changing play target to: " + ep.getTitle());
player.setCurrentEpisode(ep);
player.setCurrentPositionSec(position);
}
@Override
public void pause(PlayerContext player) {
player.setState(new PausedState());
System.out.println("[PLAYER] Transition to PAUSED at position: " + player.getCurrentPositionSec() + "s");
}
@Override
public void seek(PlayerContext player, int seconds) {
int duration = player.getCurrentEpisode().getDurationSec();
int newPos = Math.max(0, Math.min(duration, seconds));
player.setCurrentPositionSec(newPos);
System.out.println("[PLAYER] Seek to: " + newPos + "s / " + duration + "s");
}
}
class PausedState implements PlayerStateController {
@Override
public void play(PlayerContext player, Episode ep, int position) {
player.setCurrentEpisode(ep);
player.setCurrentPositionSec(position);
player.setState(new PlayingState());
System.out.println("[PLAYER] Resume PLAYING: " + ep.getTitle());
}
@Override public void pause(PlayerContext player) { System.out.println("[PLAYER] Player already paused."); }
@Override
public void seek(PlayerContext player, int seconds) {
int newPos = Math.max(0, Math.min(player.getCurrentEpisode().getDurationSec(), seconds));
player.setCurrentPositionSec(newPos);
System.out.println("[PLAYER] Seek (while paused) to: " + newPos + "s");
}
}
// Main Player Context containing state transitions
class PlayerContext {
private PlayerStateController state = new IdleState();
private Episode currentEpisode = null;
private int currentPositionSec = 0;
private double playbackSpeed = 1.0;
private final Map<String, PlaybackProgress> listenHistory = new ConcurrentHashMap<>();
private final ReentrantLock lock = new ReentrantLock();
public void setState(PlayerStateController state) {
this.lock.lock();
try {
this.state = state;
} finally {
this.lock.unlock();
}
}
public Episode getCurrentEpisode() { return currentEpisode; }
public void setCurrentEpisode(Episode ep) { this.currentEpisode = ep; }
public int getCurrentPositionSec() { return currentPositionSec; }
public void setCurrentPositionSec(int pos) {
this.currentPositionSec = pos;
if (currentEpisode != null) {
PlaybackProgress progress = listenHistory.computeIfAbsent(
currentEpisode.getId(), k -> new PlaybackProgress(currentEpisode.getId(), 0, false)
);
progress.updatePosition(pos, currentEpisode.getDurationSec());
}
}
public void setPlaybackSpeed(double speed) {
this.lock.lock();
try {
this.playbackSpeed = speed;
System.out.println("[PLAYER] Set Playback Speed to: " + speed + "x");
} finally {
this.lock.unlock();
}
}
public void play(Episode ep) {
this.lock.lock();
try {
int resumePos = 0;
if (listenHistory.containsKey(ep.getId())) {
resumePos = listenHistory.get(ep.getId()).getLastPositionSec();
System.out.println("[PLAYER] Resume request found last progress position: " + resumePos + "s");
}
state.play(this, ep, resumePos);
} finally {
this.lock.unlock();
}
}
public void pause() {
this.lock.lock();
try {
state.pause(this);
} finally {
this.lock.unlock();
}
}
public void seek(int seconds) {
this.lock.lock();
try {
state.seek(this, seconds);
} finally {
this.lock.unlock();
}
}
public PlaybackProgress getProgress(String epId) {
return listenHistory.get(epId);
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== INITIALIZING PODCAST APP ===");
DownloadManager downloadManager = new DownloadManager();
PlayerContext player = new PlayerContext();
// Seed Podcast and Episode Data
Podcast techPodcast = new Podcast("p1", "System Design Talk", "Alex");
Episode ep1 = new Episode("e1", "Introduction to Microservices", "http://audio.com/ep1.mp3", 600); // 10 min
Episode ep2 = new Episode("e2", "Understanding Kafka Shards", "http://audio.com/ep2.mp3", 900); // 15 min
techPodcast.addEpisode(ep1);
techPodcast.addEpisode(ep2);
System.out.println("\n=== 1. OFFLINE DOWNLOAD SIMULATION ===");
downloadManager.downloadEpisode(ep1);
// Wait for background download completion
Thread.sleep(1500);
System.out.println("Episode 1 Download Status: " + ep1.getDownloadStatus());
System.out.println("\n=== 2. PLAYBACK & STATE MACHINE SIMULATION ===");
// Start play ep1
player.play(ep1);
// Seek to 150 seconds
player.seek(150);
// Set speed
player.setPlaybackSpeed(1.5);
// Pause playback
player.pause();
System.out.println("\n=== 3. RESUMPTION & PROGRESS CHECKS ===");
// Re-play ep1 to ensure it resumes from 150s
player.play(ep1);
// Advance and complete (mark finished above 90% threshold)
player.seek(560); // 560s / 600s = 93.3%
player.pause();
PlaybackProgress progress = player.getProgress("e1");
System.out.println("Episode 1 Played Progress Location: " + progress.getLastPositionSec() + "s");
System.out.println("Episode 1 Completed? " + progress.isCompleted());
downloadManager.shutdown();
}
}