feat(streaming): integrate streaming playback in play_track command

- Add PlayStreaming variant to AudioCommand enum
- Add play_streaming() method to Player for starting stream-first playback
- Modify play_track command to check stream_first_track setting
- Add get_stream_info() for HEAD request to get content-length
- Add extract_audio_format_from_header() to probe audio format from initial bytes
- Add download_and_stream() for async chunked download with buffer push
- Update BufferedMediaSource with create_reader() for independent read positions
- Add has_min_buffer() method for playback readiness check
- Implement Clone for BufferWriter

Phases 3-4/7: Conditional streaming path with async download integration

cpvsc
This commit is contained in:
vicrodh committed 2026-01-25 12:39:43 -06:00
1 parent 5519fc7b25
commit 39dd249f98
3 files changed
+657 -16

No files matched your search

+272 -2
View File
@@ -7,6 +7,7 @@ use tokio::sync::Mutex;
use crate::api::client::QobuzClient;
use crate::api::models::Quality;
use crate::cache::AudioCache;
use crate::config::audio_settings::AudioSettingsState;
use crate::offline_cache::OfflineCacheState;
use crate::player::PlaybackState;
use crate::queue::QueueManager;
@@ -18,6 +19,7 @@ pub async fn play_track(
track_id: u64,
state: State<'_, AppState>,
offline_cache: State<'_, OfflineCacheState>,
audio_settings: State<'_, AudioSettingsState>,
) -> Result<(), String> {
log::info!("Command: play_track {}", track_id);
@@ -89,8 +91,20 @@ pub async fn play_track(
}
}
// Not in any cache - download and cache in memory
log::info!("Track {} not in any cache, streaming...", track_id);
// Not in any cache - check if streaming is enabled
log::info!("Track {} not in any cache, checking streaming setting...", track_id);
// Check if stream_first_track is enabled
let (stream_first_enabled, buffer_seconds) = {
let store = audio_settings.store.lock().map_err(|e| format!("Lock error: {}", e))?;
match store.get_settings() {
Ok(settings) => (settings.stream_first_track, settings.stream_buffer_seconds),
Err(e) => {
log::warn!("Failed to get audio settings, using defaults: {}", e);
(false, 3)
}
}
};
let client = state.client.lock().await;
@@ -102,6 +116,55 @@ pub async fn play_track(
log::info!("Got stream URL for track {}", track_id);
if stream_first_enabled {
// Use streaming playback - start playing before full download
log::info!("Streaming mode enabled - starting stream-first playback for track {}", track_id);
// Get content length and audio info via HEAD request
let (content_length, sample_rate, channels) = get_stream_info(&stream_url.url).await?;
log::info!(
"Stream info: {} bytes, {}Hz, {} channels",
content_length,
sample_rate,
channels
);
// Start streaming playback and get buffer writer
let buffer_writer = state.player.play_streaming(
track_id,
sample_rate,
channels,
content_length,
buffer_seconds,
)?;
// Release client lock before spawning background download
drop(client);
// Spawn background task to download and push data to buffer
let url = stream_url.url.clone();
let cache_clone = cache.clone();
tokio::spawn(async move {
match download_and_stream(&url, buffer_writer, track_id, cache_clone).await {
Ok(()) => log::info!("Streaming download complete for track {}", track_id),
Err(e) => log::error!("Streaming download failed for track {}: {}", track_id, e),
}
});
// Prefetch next track in background
spawn_prefetch(
state.client.clone(),
state.audio_cache.clone(),
&state.queue,
);
return Ok(());
}
// Standard download path (streaming disabled)
log::info!("Standard download mode for track {}", track_id);
// Download the audio
let audio_data = download_audio(&stream_url.url).await?;
let data_size = audio_data.len();
@@ -215,6 +278,213 @@ async fn download_audio(url: &str) -> Result<Vec<u8>, String> {
Ok(bytes.to_vec())
}
/// Get stream info (content length, sample rate, channels) via HEAD request and initial bytes
async fn get_stream_info(url: &str) -> Result<(u64, u32, u16), String> {
use std::time::Duration;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.build()
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
// First, do a HEAD request to get content length
let head_response = client
.head(url)
.header("User-Agent", "Mozilla/5.0")
.send()
.await
.map_err(|e| format!("Failed HEAD request: {}", e))?;
if !head_response.status().is_success() {
return Err(format!("HEAD request failed: {}", head_response.status()));
}
let content_length = head_response
.headers()
.get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.ok_or_else(|| "No content-length header".to_string())?;
// Download first ~64KB to probe audio format
// This is enough for FLAC/M4A headers
let range_response = client
.get(url)
.header("User-Agent", "Mozilla/5.0")
.header("Range", "bytes=0-65535")
.send()
.await
.map_err(|e| format!("Failed range request: {}", e))?;
if !range_response.status().is_success() && range_response.status() != reqwest::StatusCode::PARTIAL_CONTENT {
return Err(format!("Range request failed: {}", range_response.status()));
}
let initial_bytes = range_response
.bytes()
.await
.map_err(|e| format!("Failed to read initial bytes: {}", e))?;
// Try to extract audio format from initial bytes
let (sample_rate, channels) = extract_audio_format_from_header(&initial_bytes)?;
Ok((content_length, sample_rate, channels))
}
/// Extract sample rate and channels from audio file header
fn extract_audio_format_from_header(data: &[u8]) -> Result<(u32, u16), String> {
use std::io::{BufReader, Cursor};
use rodio::Decoder;
// Check if this is FLAC
if data.len() >= 4 && &data[0..4] == b"fLaC" {
// Parse FLAC STREAMINFO block
// FLAC format: "fLaC" + METADATA_BLOCK_HEADER (4 bytes) + STREAMINFO
if data.len() >= 26 {
// STREAMINFO starts at byte 8
// Bytes 10-12: sample rate (20 bits) + channels (3 bits) + bits per sample (5 bits)
let sr_high = ((data[18] as u32) << 12) | ((data[19] as u32) << 4) | ((data[20] as u32) >> 4);
let sample_rate = sr_high;
let channels = ((data[20] >> 1) & 0x07) + 1;
return Ok((sample_rate, channels as u16));
}
}
// Check if this is M4A/MP4 (ftyp box)
if data.len() >= 12 && &data[4..8] == b"ftyp" {
// M4A uses AAC or ALAC, try symphonia probe
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use symphonia::default::get_probe;
let cursor = Box::new(Cursor::new(data.to_vec())) as Box<dyn std::io::Read + Send + Sync>;
// For probing, we need to create a MediaSource
// Use a simple wrapper
struct ProbeSource {
inner: Cursor<Vec<u8>>,
len: u64,
}
impl std::io::Read for ProbeSource {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.read(buf)
}
}
impl std::io::Seek for ProbeSource {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}
impl symphonia::core::io::MediaSource for ProbeSource {
fn is_seekable(&self) -> bool { true }
fn byte_len(&self) -> Option<u64> { Some(self.len) }
}
let len = data.len() as u64;
let source = Box::new(ProbeSource { inner: Cursor::new(data.to_vec()), len });
let mss = MediaSourceStream::new(source, Default::default());
let mut hint = Hint::new();
hint.with_extension("m4a");
let format_opts = FormatOptions {
enable_gapless: true,
..Default::default()
};
if let Ok(probed) = get_probe().format(&hint, mss, &format_opts, &MetadataOptions::default()) {
if let Some(track) = probed.format.default_track() {
let sample_rate = track.codec_params.sample_rate.unwrap_or(44100);
let channels = track.codec_params.channels.map(|c| c.count() as u16).unwrap_or(2);
return Ok((sample_rate, channels));
}
}
// Default to common values for M4A
return Ok((44100, 2));
}
// Try rodio decoder for other formats
match Decoder::new(BufReader::new(Cursor::new(data.to_vec()))) {
Ok(decoder) => {
use rodio::Source;
Ok((decoder.sample_rate(), decoder.channels()))
}
Err(_) => {
// Default fallback
log::warn!("Could not determine audio format, using defaults (44100Hz, stereo)");
Ok((44100, 2))
}
}
}
/// Download audio chunks and stream them to the buffer writer
/// Also caches the complete data when download finishes
async fn download_and_stream(
url: &str,
writer: crate::player::BufferWriter,
track_id: u64,
cache: Arc<AudioCache>,
) -> Result<(), String> {
use std::time::Duration;
use futures_util::StreamExt;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(300)) // Longer timeout for streaming
.connect_timeout(Duration::from_secs(10))
.build()
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
log::info!("Starting streaming download for track {}", track_id);
let response = client
.get(url)
.header("User-Agent", "Mozilla/5.0")
.send()
.await
.map_err(|e| format!("Failed to start stream: {}", e))?;
if !response.status().is_success() {
return Err(format!("Stream request failed: {}", response.status()));
}
let mut all_data = Vec::new();
let mut stream = response.bytes_stream();
let mut bytes_received = 0u64;
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(|e| format!("Stream chunk error: {}", e))?;
bytes_received += chunk.len() as u64;
// Accumulate for caching
all_data.extend_from_slice(&chunk);
// Push to streaming buffer
if let Err(e) = writer.push_chunk(&chunk) {
log::error!("Failed to push chunk to buffer: {}", e);
}
// Log progress every ~1MB
if bytes_received % (1024 * 1024) < chunk.len() as u64 {
log::debug!("Streaming download progress: {} bytes", bytes_received);
}
}
// Mark stream as complete
if let Err(e) = writer.complete() {
log::error!("Failed to mark buffer complete: {}", e);
}
log::info!("Streaming download complete: {} bytes total", bytes_received);
// Cache the complete file for future plays
cache.insert(track_id, all_data);
Ok(())
}
/// Number of Qobuz tracks to prefetch (not total tracks, just Qobuz)
const QOBUZ_PREFETCH_COUNT: usize = 3;
+339
View File
@@ -51,6 +51,15 @@ enum AudioCommand {
sample_rate: u32,
channels: u16,
},
/// Play from streaming source (BufferedMediaSource)
PlayStreaming {
source: Arc<BufferedMediaSource>,
writer: BufferWriter,
track_id: u64,
sample_rate: u32,
channels: u16,
content_length: u64,
},
/// Pause playback
Pause,
/// Resume playback
@@ -180,6 +189,96 @@ fn decode_with_symphonia(data: &[u8]) -> Result<AudioSpecs, String> {
})
}
/// Decode audio from a streaming source (BufferedMediaSource).
/// Returns decoded samples as they become available.
/// This allows playback to start before the full file is downloaded.
fn decode_streaming_source(
source: &BufferedMediaSource,
) -> Result<AudioSpecs, String> {
// Create a new reader from the source - shares buffer but has its own read position
let reader = source.create_reader();
let media_source = Box::new(reader) as Box<dyn MediaSource>;
let mss = MediaSourceStream::new(media_source, Default::default());
let mut hint = Hint::new();
hint.with_extension("m4a");
let format_opts = FormatOptions {
enable_gapless: true,
..Default::default()
};
let metadata_opts: MetadataOptions = Default::default();
let mut probed = get_probe()
.format(&hint, mss, &format_opts, &metadata_opts)
.map_err(|err| format!("Symphonia probe failed for streaming: {}", err))?;
let track = probed
.format
.default_track()
.ok_or_else(|| "Symphonia: no supported audio tracks in stream".to_string())?;
let track_id = track.id;
let codec_params = track.codec_params.clone();
let mut decoder = get_codecs()
.make(&codec_params, &DecoderOptions::default())
.map_err(|err| format!("Symphonia decoder init failed for streaming: {}", err))?;
let mut sample_rate = 0;
let mut channels = 0u16;
let mut samples: Vec<i16> = Vec::new();
loop {
let packet = match probed.format.next_packet() {
Ok(packet) => packet,
Err(SymphoniaError::IoError(e)) => {
// Check if we got WouldBlock (not enough data buffered yet)
if e.kind() == std::io::ErrorKind::WouldBlock {
// Wait for more data
std::thread::sleep(Duration::from_millis(10));
continue;
}
// EOF or other IO error - we're done
break;
}
Err(err) => return Err(format!("Symphonia read error in stream: {}", err)),
};
if packet.track_id() != track_id {
continue;
}
match decoder.decode(&packet) {
Ok(audio_buf) => {
let spec = *audio_buf.spec();
if sample_rate == 0 {
sample_rate = spec.rate;
channels = spec.channels.count() as u16;
}
let mut sample_buf = SampleBuffer::<i16>::new(audio_buf.frames() as u64, spec);
sample_buf.copy_interleaved_ref(audio_buf);
samples.extend_from_slice(sample_buf.samples());
}
Err(SymphoniaError::DecodeError(_)) => continue,
Err(SymphoniaError::ResetRequired) => {
decoder.reset();
continue;
}
Err(err) => return Err(format!("Symphonia decode error in stream: {}", err)),
}
}
if samples.is_empty() || sample_rate == 0 || channels == 0 {
return Err("Symphonia streaming decode produced no audio".to_string());
}
Ok(AudioSpecs {
samples: SamplesBuffer::new(channels, sample_rate, samples),
sample_rate,
channels,
})
}
fn is_isomp4(data: &[u8]) -> bool {
if data.len() < 12 {
return false;
@@ -1049,6 +1148,204 @@ impl Player {
actual_duration
);
}
AudioCommand::PlayStreaming { source, writer: _, track_id, sample_rate, channels, content_length } => {
log::info!(
"Audio thread: starting streaming playback for track {} ({}Hz, {} channels, {} bytes)",
track_id,
sample_rate,
channels,
content_length
);
*pause_suspend_deadline = None;
// Get DAC passthrough setting
let dac_passthrough = thread_settings
.lock()
.ok()
.map(|s| s.dac_passthrough)
.unwrap_or(false);
// Check if we need to recreate the stream
let format_changed = *current_sample_rate != Some(sample_rate)
|| *current_channels != Some(channels);
let using_alsa_direct = thread_settings
.lock()
.ok()
.and_then(|s| s.backend_type)
.map(|b| b == AudioBackendType::Alsa)
.unwrap_or(false);
let needs_new_stream = stream_opt.is_none()
|| (dac_passthrough && format_changed)
|| (using_alsa_direct && format_changed);
if needs_new_stream {
if stream_opt.is_some() {
if (dac_passthrough || using_alsa_direct) && format_changed {
let mode = if using_alsa_direct { "ALSA Direct" } else { "DAC passthrough" };
log::info!(
"Streaming: Sample rate/channels changed to {}Hz/{}ch - recreating OutputStream ({})",
sample_rate,
channels,
mode
);
}
drop(stream_opt.take());
}
let stream_result = if let Some(settings) = thread_settings.lock().ok() {
match try_init_stream_with_backend(&settings, sample_rate, channels) {
Some(result) => result,
None => {
log::info!("Backend system not configured, using legacy CPAL path");
let device = if let Some(ref name) = *current_device_name {
host.output_devices()
.ok()
.and_then(|mut devices| {
devices.find(|d| d.name().ok().as_ref() == Some(name))
})
.or_else(|| host.default_output_device())
} else {
host.default_output_device()
};
let Some(device) = device else {
log::error!("No audio output device available for streaming");
thread_state.set_stream_error(true);
return;
};
if let Ok(name) = device.name() {
thread_state.set_current_device(Some(name));
}
create_output_stream_with_config(
&device,
sample_rate,
channels,
dac_passthrough,
).map(|(stream, handle)| StreamType::Rodio(stream, handle))
}
}
} else {
let device = host.default_output_device();
let Some(device) = device else {
log::error!("No audio output device available for streaming");
thread_state.set_stream_error(true);
return;
};
create_output_stream_with_config(&device, sample_rate, channels, dac_passthrough)
.map(|(stream, handle)| StreamType::Rodio(stream, handle))
};
match stream_result {
Ok(stream) => {
*stream_opt = Some(stream);
*current_sample_rate = Some(sample_rate);
*current_channels = Some(channels);
thread_state.set_stream_error(false);
log::info!("✅ Streaming audio stream ready at {}Hz", sample_rate);
std::thread::sleep(Duration::from_millis(150));
}
Err(e) => {
log::error!("❌ Failed to create stream for streaming at {}Hz: {}", sample_rate, e);
thread_state.set_stream_error(true);
return;
}
}
}
let Some(ref stream) = *stream_opt else {
log::error!("Audio thread: no audio device available for streaming");
return;
};
// Stop previous engine
if let Some(engine) = current_engine.take() {
engine.stop();
std::thread::sleep(Duration::from_millis(50));
}
// Create PlaybackEngine
let mut engine = match stream {
StreamType::Rodio(_stream, handle) => {
match PlaybackEngine::new_rodio(handle) {
Ok(e) => {
*consecutive_sink_failures = 0;
thread_state.set_stream_error(false);
e
}
Err(e) => {
log::error!("Failed to create engine for streaming: {}", e);
return;
}
}
}
#[cfg(target_os = "linux")]
StreamType::AlsaDirect(alsa_stream) => {
let hardware_volume = thread_settings
.lock()
.ok()
.map(|s| s.alsa_hardware_volume)
.unwrap_or(false);
PlaybackEngine::new_alsa_direct(alsa_stream.clone(), hardware_volume)
}
};
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
engine.set_volume(volume);
// Wait for minimum buffer before decoding
log::info!("Streaming: waiting for initial buffer...");
let start_wait = Instant::now();
let max_wait = Duration::from_secs(30);
while !source.has_min_buffer() && start_wait.elapsed() < max_wait {
std::thread::sleep(Duration::from_millis(50));
}
if !source.has_min_buffer() {
log::error!("Streaming: timeout waiting for initial buffer");
return;
}
log::info!("Streaming: initial buffer ready, starting decode...");
// Decode from streaming source (pass reference, it uses create_reader internally)
let decoded = match decode_streaming_source(&source) {
Ok(specs) => specs,
Err(e) => {
log::error!("Failed to decode streaming audio: {}", e);
return;
}
};
let actual_duration = decoded.samples
.total_duration()
.map(|d| d.as_secs())
.unwrap_or(0);
thread_state.duration.store(actual_duration, Ordering::SeqCst);
// The samples are already a SamplesBuffer which implements Source
let source_to_play: Box<dyn Source<Item = i16> + Send> = Box::new(decoded.samples);
if let Err(e) = engine.append(source_to_play) {
log::error!("Failed to append streaming source to engine: {}", e);
return;
}
thread_state.is_playing.store(true, Ordering::SeqCst);
thread_state.position.store(0, Ordering::SeqCst);
thread_state.current_track_id.store(track_id, Ordering::SeqCst);
thread_state.start_playback_timer(0);
*current_engine = Some(engine);
// Note: We don't store streaming data in current_audio_data since it's handled differently
log::info!(
"Audio thread: streaming playback started, duration: {}s",
actual_duration
);
}
AudioCommand::Pause => {
if let Some(ref engine) = *current_engine {
engine.pause();
@@ -1441,6 +1738,48 @@ impl Player {
Ok(())
}
/// Play from streaming source (starts playback before full download)
/// Returns the BufferWriter so caller can push data as it downloads
pub fn play_streaming(
&self,
track_id: u64,
sample_rate: u32,
channels: u16,
content_length: u64,
buffer_seconds: u8,
) -> Result<BufferWriter, String> {
log::info!(
"Player: Starting streaming playback for track {} ({}Hz, {}ch, {} bytes total)",
track_id,
sample_rate,
channels,
content_length
);
// Use StreamingConfig::from_seconds for proper buffer sizing
let config = StreamingConfig::from_seconds(buffer_seconds);
let (source, writer) = BufferedMediaSource::new(config, Some(content_length));
let source = Arc::new(source);
self.tx
.send(AudioCommand::PlayStreaming {
source: source.clone(),
writer: writer.clone(),
track_id,
sample_rate,
channels,
content_length,
})
.map_err(|e| {
log::error!("Player: Failed to send streaming command: {}", e);
format!("Failed to send streaming play command: {}", e)
})?;
log::info!("Player: Streaming playback initiated");
Ok(writer)
}
/// Download audio from URL with timeout
async fn download_audio(&self, url: &str) -> Result<Vec<u8>, String> {
use std::time::Duration;
+46 -14
View File
@@ -63,8 +63,6 @@ impl StreamingConfig {
struct BufferState {
/// Accumulated data from HTTP response
data: Vec<u8>,
/// Current read position
read_pos: usize,
/// True when HTTP download is complete
download_complete: bool,
/// Error from download, if any
@@ -81,6 +79,8 @@ struct BufferState {
pub struct BufferedMediaSource {
state: Arc<(Mutex<BufferState>, Condvar)>,
config: StreamingConfig,
/// Each reader has its own read position
read_pos: std::sync::atomic::AtomicU64,
}
impl BufferedMediaSource {
@@ -92,7 +92,6 @@ impl BufferedMediaSource {
let state = Arc::new((
Mutex::new(BufferState {
data: Vec::with_capacity(config.initial_buffer_bytes),
read_pos: 0,
download_complete: false,
download_error: None,
total_size,
@@ -102,7 +101,8 @@ impl BufferedMediaSource {
let source = Self {
state: Arc::clone(&state),
config,
config: config.clone(),
read_pos: std::sync::atomic::AtomicU64::new(0),
};
let writer = BufferWriter { state };
@@ -110,6 +110,16 @@ impl BufferedMediaSource {
(source, writer)
}
/// Create a new reader that shares the same buffer but has its own read position.
/// This is used to pass to symphonia which needs ownership of the reader.
pub fn create_reader(&self) -> Self {
Self {
state: Arc::clone(&self.state),
config: self.config.clone(),
read_pos: std::sync::atomic::AtomicU64::new(0),
}
}
/// Wait until initial buffer is filled or download completes.
///
/// This should be called before passing the source to the decoder,
@@ -192,17 +202,34 @@ impl BufferedMediaSource {
None
}
}
/// Check if minimum buffer for playback is available
///
/// Returns true when initial_buffer_bytes have been buffered
/// or the download is complete.
pub fn has_min_buffer(&self) -> bool {
let (lock, _) = &*self.state;
if let Ok(state) = lock.lock() {
state.data.len() >= self.config.initial_buffer_bytes || state.download_complete
} else {
false
}
}
}
impl Read for BufferedMediaSource {
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
use std::sync::atomic::Ordering;
let (lock, cvar) = &*self.state;
let mut state = lock.lock().map_err(|_| {
IoError::new(ErrorKind::Other, "Failed to acquire buffer lock")
})?;
let read_pos = self.read_pos.load(Ordering::SeqCst) as usize;
// Wait for data if we're ahead of buffer
while state.read_pos >= state.data.len()
while read_pos >= state.data.len()
&& !state.download_complete
&& state.download_error.is_none()
{
@@ -217,15 +244,15 @@ impl Read for BufferedMediaSource {
}
// EOF if at end and download complete
if state.read_pos >= state.data.len() && state.download_complete {
if read_pos >= state.data.len() && state.download_complete {
return Ok(0);
}
// Read available data
let available = state.data.len() - state.read_pos;
let available = state.data.len() - read_pos;
let to_read = buf.len().min(available);
buf[..to_read].copy_from_slice(&state.data[state.read_pos..state.read_pos + to_read]);
state.read_pos += to_read;
buf[..to_read].copy_from_slice(&state.data[read_pos..read_pos + to_read]);
self.read_pos.store((read_pos + to_read) as u64, Ordering::SeqCst);
Ok(to_read)
}
@@ -233,14 +260,18 @@ impl Read for BufferedMediaSource {
impl Seek for BufferedMediaSource {
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
use std::sync::atomic::Ordering;
let (lock, cvar) = &*self.state;
let mut state = lock.lock().map_err(|_| {
IoError::new(ErrorKind::Other, "Failed to acquire buffer lock")
})?;
let current_pos = self.read_pos.load(Ordering::SeqCst) as i64;
let new_pos = match pos {
SeekFrom::Start(offset) => offset as i64,
SeekFrom::Current(offset) => state.read_pos as i64 + offset,
SeekFrom::Current(offset) => current_pos + offset,
SeekFrom::End(offset) => {
// For End seeks, we need to know total size or have complete download
if let Some(total) = state.total_size {
@@ -264,10 +295,10 @@ impl Seek for BufferedMediaSource {
));
}
let new_pos = new_pos as usize;
let new_pos_usize = new_pos as usize;
// If seeking forward beyond buffer, wait for data
while new_pos > state.data.len()
while new_pos_usize > state.data.len()
&& !state.download_complete
&& state.download_error.is_none()
{
@@ -281,14 +312,14 @@ impl Seek for BufferedMediaSource {
}
// After download complete, check bounds
if state.download_complete && new_pos > state.data.len() {
if state.download_complete && new_pos_usize > state.data.len() {
return Err(IoError::new(
ErrorKind::InvalidInput,
"Seek position beyond end of stream",
));
}
state.read_pos = new_pos;
self.read_pos.store(new_pos as u64, Ordering::SeqCst);
Ok(new_pos as u64)
}
}
@@ -314,6 +345,7 @@ impl MediaSource for BufferedMediaSource {
///
/// This is the sender side that receives data from the HTTP response
/// and makes it available to the `BufferedMediaSource` reader.
#[derive(Clone)]
pub struct BufferWriter {
state: Arc<(Mutex<BufferState>, Condvar)>,
}