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
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
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:Every episode has an
audio_url: Stringfield (models.rs:65) that points to the media file.Proposed Solution
1. HTTP Streaming Source
Create a custom rodio
Sourcethat reads from an HTTP stream:2. Buffering Strategy
3. PlaybackBackend Extension
Add to
PlaybackBackendtrait: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 fileFiles to Modify
src/audio/mod.rsplay_url()to PlaybackBackend traitsrc/audio/rodio_backend.rssrc/audio/http_stream.rssrc/ui/buffers/now_playing.rssrc/ui/mod.rsStreamEpisodeUIAction variantAcceptance Criteria
audio_urlwithout downloadingImplementation Notes
.bytes_stream()returnsimpl Stream<Item = Result<Bytes>>— need to adapt to rodio's synchronousSourcetraittokio::sync::mpscto bridge async HTTP stream to sync audio threadRange: bytes=N-M) enable seeking in HTTP streams (if server supports it)mpv <url>works out of the box