Machine Coding Problem

Podcast Player

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

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