Machine Coding Problem

Podcast Player

maco60macoAllmediastate-(play/pause)playlists
Commonly Asked By:SpotifyAppleGoogle

Requirements & System Scope

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.5xโ€“3x) 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.

Class Diagram & Entity Relationships

Structural layout showing relationships between players, state machines, and listen histories:

Loading...
  • State Trackers: Player manages current playback states, media speeds, and timestamps.
  • Episode Metadata: Episode keeps track of download statuses, durations, and audio URLs.

Design Patterns & SOLID Principles

  • State Pattern (Player States): Encapsulating player state transitions (IDLE, PLAYING, PAUSED) isolates playback logic from state validation, satisfying the Open-Closed Principle.
  • Single Responsibility Principle (SRP):Player handles audio controls; DownloadManager manages offline downloads; PlaybackProgress records listen metrics.

Core Execution Workflows

Playback & State Changes

  1. User selects an episode: play(episodeId).
  2. Look up the last played position in the user's ListenHistory.
  3. Transition the player state to PLAYING.
  4. Update play timestamps as progress is made.
  5. If progress reaches 90% or more, mark the episode as fully complete.
  6. On pause, save the current position in the user's history and transition the player state to PAUSED.

Concurrency & Thread Safety Strategy

When users stream and manage history across multiple devices concurrently:

  • Synchronized State Transitions: Synchronize player state transitions using thread locks (`ReentrantLock`) to prevent race conditions from concurrent playback commands.
  • Thread-Safe Map Storage: Store user history in thread-safe maps (e.g., ConcurrentHashMap) to allow safe parallel reads and writes.

Complete Clean Code Blueprint

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();
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...