feat: implement functional seek in audio player

- Add Seek command to AudioCommand enum
- Store audio data in audio thread for re-seeking
- Use rodio's skip_duration to seek to desired position
- Preserve play/pause state after seeking
- Update playback timer to account for seek position
This commit is contained in:
vicrodh committed 2026-01-10 04:21:56 -06:00
1 parent baa656a508
commit 95488014a6
1 file changed
+78 -6
+78 -6
View File
@@ -31,6 +31,8 @@ enum AudioCommand {
Stop,
/// Set volume (0.0 - 1.0)
SetVolume(f32),
/// Seek to position in seconds
Seek(u64),
}
/// Event payload for playback state updates
@@ -195,6 +197,8 @@ impl Player {
};
let mut current_sink: Option<Sink> = None;
// Store audio data for seeking (we need to re-decode from the beginning)
let mut current_audio_data: Option<Vec<u8>> = None;
log::info!("Audio thread ready and waiting for commands");
@@ -209,6 +213,9 @@ impl Player {
sink.stop();
}
// Store audio data for potential seeking
current_audio_data = Some(data.clone());
// Create new sink
let sink = match Sink::try_new(&stream_handle) {
Ok(s) => s,
@@ -270,6 +277,7 @@ impl Player {
if let Some(sink) = current_sink.take() {
sink.stop();
}
current_audio_data = None;
thread_state.is_playing.store(false, Ordering::SeqCst);
thread_state.position.store(0, Ordering::SeqCst);
thread_state.playback_start_millis.store(0, Ordering::SeqCst);
@@ -285,6 +293,63 @@ impl Player {
}
log::info!("Audio thread: volume set to {}", volume);
}
Ok(AudioCommand::Seek(position_secs)) => {
let Some(ref audio_data) = current_audio_data else {
log::warn!("Audio thread: cannot seek - no audio data available");
continue;
};
log::info!("Audio thread: seeking to {}s", position_secs);
// Stop current sink
if let Some(sink) = current_sink.take() {
sink.stop();
}
// Create new sink
let sink = match Sink::try_new(&stream_handle) {
Ok(s) => s,
Err(e) => {
log::error!("Failed to create sink for seek: {}", e);
continue;
}
};
// Set volume
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
sink.set_volume(volume);
// Re-decode audio
let cursor = Cursor::new(audio_data.clone());
let source = match Decoder::new(BufReader::new(cursor)) {
Ok(s) => s,
Err(e) => {
log::error!("Failed to decode audio for seek: {}", e);
continue;
}
};
// Skip to the desired position
let skip_duration = Duration::from_secs(position_secs);
let skipped_source = source.skip_duration(skip_duration);
sink.append(skipped_source);
// Check if we were playing before seek
let was_playing = thread_state.is_playing.load(Ordering::SeqCst);
if !was_playing {
sink.pause();
}
// Update state
thread_state.position.store(position_secs, Ordering::SeqCst);
if was_playing {
thread_state.start_playback_timer(position_secs);
}
current_sink = Some(sink);
log::info!("Audio thread: seeked to {}s (was_playing: {})", position_secs, was_playing);
}
Err(RecvTimeoutError::Timeout) => {
// Check if sink is empty (playback finished)
if let Some(ref sink) = current_sink {
@@ -426,12 +491,19 @@ impl Player {
.map_err(|e| format!("Failed to send volume command: {}", e))
}
/// Seek to position (not supported for streaming, but we track position)
pub fn seek(&self, _position: u64) -> Result<(), String> {
// Seeking in streaming audio is complex - for now just update the position tracker
// Real seeking would require re-downloading from a specific byte offset
log::warn!("Seek not fully implemented for streaming audio");
Ok(())
/// Seek to position in seconds
pub fn seek(&self, position: u64) -> Result<(), String> {
// Clamp to duration if known
let duration = self.state.duration();
let clamped_position = if duration > 0 {
position.min(duration)
} else {
position
};
self.tx
.send(AudioCommand::Seek(clamped_position))
.map_err(|e| format!("Failed to send seek command: {}", e))
}
/// Get current playback state with real-time position