Finding
AndroidEngine::seek validates that playback is not stopped, emits a SeekCompleted event, and returns the clamped position — but it never communicates the requested position to the running playback_task. The task drives playback by looping over decoder.next_frame(); there is no seek channel, atomic, or any other signal it consumes. The decoder continues from wherever its internal cursor already sits, so the audio position is unchanged. The method reports success for an operation it does not perform.
Evidence
crates/akouo-android/src/lib.rs:247-259 — the entire body after the state guard only emits an event and returns:
pub async fn seek(&self, position_secs: f64) -> Result<f64, AndroidEngineError> {
if self.state.load(Ordering::SeqCst) == STATE_STOPPED {
return Err(AndroidEngineError::NotPlaying);
}
self.notify(AndroidEngineEvent {
kind: AndroidEngineEventKind::SeekCompleted,
path: None,
message: None,
position_secs: Some(position_secs.max(0.0)),
underrun_count: None,
});
Ok(position_secs.max(0.0))
}
No channel send, no atomic store, no handle to playback_task is touched.
crates/akouo-android/src/lib.rs:345-359 — the playback loop only checks STATE_STOPPED/STATE_PAUSED and otherwise pulls the next decoded frame; it has no path to accept a target position:
loop {
match context.state.load(Ordering::SeqCst) {
STATE_STOPPED => break,
STATE_PAUSED => {
tokio::time::sleep(PAUSE_SLEEP).await;
continue;
}
_ => {}
}
let frame = match decoder.next_frame().await {
Ok(Some(frame)) => frame,
Ok(None) => break,
Err(e) => return Err(e.to_string()),
};
Why this matters
Every seek request from the UI silently no-ops while reporting success. The seek bar appears responsive — a SeekCompleted event fires and the returned position echoes the request — but the audio stream never moves. The mismatch between the reported state and the actual decoder position desynchronizes any caller that trusts the returned value (progress display, resume-position persistence), and for long-form content (audiobooks, podcasts) it disables the primary navigation control entirely. A success result that does not reflect real engine state is also a correctness hazard for higher layers that branch on it.
Desired correction
Give the engine a way to push a target position into the running task — e.g. a watch::Sender<Option<f64>> (or equivalent) owned by AndroidEngine and read inside the playback_task loop. seek() sends the clamped target; the task observes the new value, calls decoder.seek(position), drops any in-flight buffered frames, and only then emits SeekCompleted reflecting the position the decoder actually reached. If no decoder seek is available, seek() must return an error rather than a fabricated success. Done when: calling seek(t) on a playing engine causes decoder.next_frame() to resume from position t (verified by a test asserting the post-seek frame timestamp/sample offset corresponds to t), and SeekCompleted carries the decoder's actual resumed position.
Finding
AndroidEngine::seekvalidates that playback is not stopped, emits aSeekCompletedevent, and returns the clamped position — but it never communicates the requested position to the runningplayback_task. The task drives playback by looping overdecoder.next_frame(); there is no seek channel, atomic, or any other signal it consumes. The decoder continues from wherever its internal cursor already sits, so the audio position is unchanged. The method reports success for an operation it does not perform.Evidence
crates/akouo-android/src/lib.rs:247-259— the entire body after the state guard only emits an event and returns:No channel send, no atomic store, no handle to
playback_taskis touched.crates/akouo-android/src/lib.rs:345-359— the playback loop only checksSTATE_STOPPED/STATE_PAUSEDand otherwise pulls the next decoded frame; it has no path to accept a target position:Why this matters
Every seek request from the UI silently no-ops while reporting success. The seek bar appears responsive — a
SeekCompletedevent fires and the returned position echoes the request — but the audio stream never moves. The mismatch between the reported state and the actual decoder position desynchronizes any caller that trusts the returned value (progress display, resume-position persistence), and for long-form content (audiobooks, podcasts) it disables the primary navigation control entirely. A success result that does not reflect real engine state is also a correctness hazard for higher layers that branch on it.Desired correction
Give the engine a way to push a target position into the running task — e.g. a
watch::Sender<Option<f64>>(or equivalent) owned byAndroidEngineand read inside theplayback_taskloop.seek()sends the clamped target; the task observes the new value, callsdecoder.seek(position), drops any in-flight buffered frames, and only then emitsSeekCompletedreflecting the position the decoder actually reached. If no decoder seek is available,seek()must return an error rather than a fabricated success. Done when: callingseek(t)on a playing engine causesdecoder.next_frame()to resume from positiont(verified by a test asserting the post-seek frame timestamp/sample offset corresponds tot), andSeekCompletedcarries the decoder's actual resumed position.