Skip to content

[Feature] Streaming playback from RSS audio URL #149

Description

@lqdev

Summary

Enable playing episodes directly from their RSS audio URL without downloading first. This adds a streaming mode alongside the existing download-then-play workflow.

Related: #131 (Meta-Epic: Cross-Platform Audio Playback)

Problem / Background

The MVP requires episodes to be downloaded (episode.local_path.is_some()) before playing. This means:

  • Users must wait for full download before listening
  • Storage space used for episodes they may only listen to once
  • No "preview" functionality for deciding whether to subscribe

Every episode has an audio_url: String field (models.rs:65) that points to the media file.

Proposed Solution

1. HTTP Streaming Source

Create a custom rodio Source that reads from an HTTP stream:

pub struct HttpStreamSource {
    client: reqwest::Client,
    response: reqwest::Response,
    decoder: Decoder<BufReader<StreamReader>>,
    buffer: Vec<u8>,
    buffer_pos: usize,
}

2. Buffering Strategy

  • Pre-buffer N seconds of audio before playback starts (configurable, default 5s)
  • Continue buffering ahead while playing
  • Handle network interruptions with retry logic
  • Show buffer state in NowPlaying UI (buffer bar + playback bar)

3. PlaybackBackend Extension

Add to PlaybackBackend trait:

fn play_url(&mut self, url: &str) -> Result<(), AudioError>;
fn is_buffering(&self) -> bool;
fn buffer_progress(&self) -> Option<f64>; // 0.0 to 1.0

4. Cache While Streaming (Optional)

Write streamed bytes to disk as a side effect, so the episode is downloaded by the time playback finishes:

// Tee the stream: decode for playback + write to file

Files to Modify

File Change
src/audio/mod.rs Add play_url() to PlaybackBackend trait
src/audio/rodio_backend.rs Implement HTTP streaming source
src/audio/http_stream.rs New — HTTP streaming + buffering logic
src/ui/buffers/now_playing.rs Show buffer progress indicator
src/ui/mod.rs Add StreamEpisode UIAction variant

Acceptance Criteria

  • Can play an episode from its audio_url without downloading
  • Buffering indicator shows in NowPlaying buffer
  • Handles network interruptions gracefully (retry, show error)
  • Seek works within buffered range
  • Optional: cache-while-streaming saves file to disk

Implementation Notes

  • reqwest's .bytes_stream() returns impl Stream<Item = Result<Bytes>> — need to adapt to rodio's synchronous Source trait
  • Consider using tokio::sync::mpsc to bridge async HTTP stream to sync audio thread
  • Byte-range requests (Range: bytes=N-M) enable seeking in HTTP streams (if server supports it)
  • This is XL effort — consider as a separate phase or even separate epic
  • External player backend could implement streaming trivially: mpv <url> works out of the box

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low priorityaudioAudio playback componentenhancementNew feature or request

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions