android auto: the position the engine is actually at, and search by voice
**Pausing looked like the track restarting, and it was not only a look.** The card carried the position it was BUILT with — zero, on the headless path, because nothing wrote it again. media3 extrapolates a position while playing, so the bar moved and seemed right, but every state rebuild snapped back to that stale zero. Worse than cosmetic: media3 then BELIEVED the player was at zero, and reconciled on resume by seeking there, so the track really did start again. The same staleness made a seek look like it had done nothing — the engine moved and the next rebuild reported zero. `getState()` asks the engine now, on every rebuild, through `positionMs()` and `sounding()`. One JNI call and always true, instead of a number remembered from the moment playback began. A seek moves the reported position immediately rather than waiting for the engine's next event, so the scrubber does not lag a beat behind and read as snapping back. **Search, which is the half of round fifty-two that needed no decisions.** `onSearch` answers the count and `onGetSearchResult` the rows; both run the same couple of LIKEs over the library, because there is no search index in this schema and a few hundred albums do not need one. Albums rank before tracks: "play Discovery" almost always means the record, and whoever wanted the song would have said more — which matters because a voice command takes the first result. Voice comes in by a different door and it is implemented too. "Play X" arrives at `onAddMediaItems` as an item with NO media id and a `searchQuery` instead; Google's own samples disagree about whether Auto uses that or `onSearch`, so both are answered. An empty query is "play some music" and gets Recent rather than an error. The prober gained a `search:` prefix for it — `probe.sh 2 false "search:discovery"` issues `playFromSearch` through the session token, which is the path a spoken sentence actually takes. It answers `auto voice 'discovery' n=1`, `auto play ss:album:2 queued=14`, and the controller reports "One More Time" playing: a sentence turned into a record, in a process with no Activity. Still library-only, and that is now the whole remaining question. Playlists are local and would play from cold exactly as albums do, blocked only on which four root slots to spend. Catalog and Soulseek rows cannot resolve without the WebView, but can whenever the app is warm — browsable always, an honest error only when cold.
This commit is contained in:
1 parent
4d75395411
commit
f59a2d773e
6 files changed
+327
-7
No files matched your search
@@ -34,6 +34,14 @@ pub struct Audio {
|
||||
/// buffered for a UI that may never exist.
|
||||
subscriber: Mutex<Option<Sender<PlayerEvent>>>,
|
||||
queue: Mutex<Queue>,
|
||||
/// Where the engine actually is, in seconds, and whether it is sounding.
|
||||
///
|
||||
/// The card used to carry the position it was BUILT with — zero — for the
|
||||
/// life of a track. media3 extrapolates a position while playing, so the
|
||||
/// bar moved and looked right, but every state re-read snapped back to
|
||||
/// that stale zero: pausing looked like the track restarting, and a seek
|
||||
/// that really happened showed as nothing at all.
|
||||
progress: Mutex<(f64, bool)>,
|
||||
}
|
||||
|
||||
/// Enough of a track to play it and to describe it on a card, read once when
|
||||
@@ -73,6 +81,7 @@ pub fn audio(device: Option<String>) -> Arc<Audio> {
|
||||
player,
|
||||
subscriber: Mutex::new(None),
|
||||
queue: Mutex::new(Queue::default()),
|
||||
progress: Mutex::new((0.0, false)),
|
||||
});
|
||||
let pump = Arc::clone(&audio);
|
||||
let _ = std::thread::Builder::new()
|
||||
@@ -87,6 +96,26 @@ pub fn audio(device: Option<String>) -> Arc<Audio> {
|
||||
if matches!(ev, PlayerEvent::TrackEnded { .. }) {
|
||||
pump.advance();
|
||||
}
|
||||
// Keep the engine's own truth current. This is the
|
||||
// only place that knows it, and the card asks for it
|
||||
// rather than being pushed at — a position event is
|
||||
// frequent, and a JNI hop per event would cost more
|
||||
// than the card is worth.
|
||||
{
|
||||
let mut p =
|
||||
pump.progress.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match &ev {
|
||||
PlayerEvent::Position { secs, .. } => p.0 = *secs,
|
||||
PlayerEvent::Playing => p.1 = true,
|
||||
PlayerEvent::Paused => p.1 = false,
|
||||
PlayerEvent::Stopped => {
|
||||
p.0 = 0.0;
|
||||
p.1 = false;
|
||||
}
|
||||
PlayerEvent::TrackEnded { .. } => p.0 = 0.0,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let sub = pump.subscriber.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(tx) = sub.as_ref() {
|
||||
// A closed channel means the WebView went away;
|
||||
@@ -151,7 +180,10 @@ impl Audio {
|
||||
q.idx = idx;
|
||||
}
|
||||
match first {
|
||||
Some(t) => self.player.play_local(t.path, t.duration),
|
||||
Some(t) => {
|
||||
self.player.play_local(t.path, t.duration);
|
||||
*self.progress.lock().unwrap_or_else(|e| e.into_inner()) = (0.0, true);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
@@ -190,6 +222,14 @@ impl Audio {
|
||||
len - start
|
||||
}
|
||||
|
||||
/// Seek, and move the reported position with it. Waiting for the engine's
|
||||
/// next position event would leave the card showing the OLD spot for a
|
||||
/// beat, which reads as the scrubber snapping back.
|
||||
pub fn seek(&self, secs: f64) {
|
||||
self.player.seek(secs);
|
||||
self.progress.lock().unwrap_or_else(|e| e.into_inner()).0 = secs;
|
||||
}
|
||||
|
||||
/// Move by one track in the fallback queue. Returns false at either end,
|
||||
/// so the caller can leave the card alone rather than pretend.
|
||||
///
|
||||
@@ -219,6 +259,7 @@ impl Audio {
|
||||
match picked {
|
||||
Some(t) => {
|
||||
self.player.play_local(t.path.clone(), t.duration);
|
||||
*self.progress.lock().unwrap_or_else(|e| e.into_inner()) = (0.0, true);
|
||||
notify_track(&t, more);
|
||||
true
|
||||
}
|
||||
@@ -226,6 +267,11 @@ impl Audio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the engine is now: seconds, and whether it is sounding.
|
||||
pub fn progress(&self) -> (f64, bool) {
|
||||
*self.progress.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// True when this engine is the one driving playback — i.e. a car started
|
||||
/// it and no WebView has taken over. The transport uses it to decide
|
||||
/// whether a button belongs to Rust or to the JS.
|
||||
|
||||
Reference in new issue
Block a user