android: the media mirror grows a queue window the card never shows

The Kotlin side of the mirror has always known about exactly one track, and
media3's `SimpleBasePlayer.getState()` — the thing an Android Auto session is
built on — wants a playlist and an index into it. So the data lands a round
before the migration that needs it, where it cannot break anything:
`NowPlaying` and `NowPlayingArgs` carry a bounded `queue` of at most
twenty-one entries, the current item and ten either side, each with its id,
title, artist, album, duration and art key, plus `queueIndex`, where the
current one sits within that window rather than within the whole queue.
`src/mobile/media.svelte.js` fills them from `player.queue` and `player.qi`;
`MediaService` ignores every bit of it, so the card is byte for byte the card
it was. Ten either side because one covers the card's own prev and next and
nothing else while a queue list — which is what Auto asks for — wants a
screenful plus a scroll both ways, and it is a cap rather than a length: a
two-thousand-track queue crosses the bridge as twenty-one entries exactly
like a twenty-one-track one, measured at two kilobytes of JSON for twenty-one
short titles and six with long ones and real cover paths, against the thirty
to eighty of base64 cover that already ride the same call once per record.

Nothing renders it, which is the whole difficulty in checking it — and both
hops of this bridge fail silently, because serde drops an unknown field and
tauri's Jackson mapper has `FAIL_ON_UNKNOWN_PROPERTIES` off, so a key spelled
snake_case where camelCase was wanted arrives as nothing at all with no error
anywhere. `MediaSessionPlugin.update` logs one line per update, and the phone
walk holds that line against `__ss.card` — the mirror media.svelte.js now
keeps of what it last sent, the shape xfernote has kept of the shade since it
was written — field for field, through a next and a previous. The art-key
count is compared as well as the size and the index, since that is the only
camelCase field an entry has. The check arms at queue index one rather than
at wherever the walk left off, which buys a row on either side — `prev()`
seeks to zero at the top of a queue and `next()` stops at the end — a track
that has only just started, so `prev()` is still a step rather than the seek
it becomes past five seconds, and a window whose own index is the queue
index, because nothing is clamped off the front while the index is under ten.
Each of the three readings has to be a fresh one at both ends, the push
counter up and a new line in the log, since a `prev()` that lands back where
it started reads exactly like a stale line otherwise; and a queue too short
to arm, or repeat-one, under which `next()` seeks instead of stepping, fails
the check rather than skipping it.

The plugin's `consumer-rules.pro` keeps the new `@InvokeArg` class by name
beside the one it already keeps; without that line a release build would fill
`NowPlayingArgs` and leave every queue entry null, quietly. Rust and Kotlin
both changed, so this one cannot go down `dev.sh` — it needs the full
`deploy.sh` detour. And everything the walk already asserts about the card is
untouched, which is the real check on this round.
This commit is contained in:
KeelrLeek committed 2026-09-15 07:22:02 -05:00
1 parent d42645d2a0
commit afcd3cc7f3
7 files changed
+301 -7

No files matched your search

+48 -3
View File
@@ -857,6 +857,50 @@ with a `ClassNotFoundException`. `src-tauri/android/build.sh` exports it;
after adding a plugin, touch `src-tauri/build.rs` too, so the app's build
script re-runs and rewrites the wiring.
Every update also carries a **queue window** that nothing reads. `NowPlaying`
(Rust) and `NowPlayingArgs` (Kotlin) hold a `queue` of at most twenty-one
entries — the current item and ten either side, each with its id, title,
artist, album, duration and art key — plus `queueIndex`, where the current one
sits *within that window* rather than within the whole queue.
`src/mobile/media.svelte.js` fills both from `player.queue` and `player.qi`;
`MediaService` ignores them completely, so the card is byte for byte the card
it was. The reason is media3: `SimpleBasePlayer.getState()` wants a playlist
and an index into it, and the data that migration needs should have been
running on the phone for a while before anything depends on it. Ten either
side because one covers the card's own prev and next and nothing else, while a
queue *list* — which is what Auto asks for — wants a screenful plus a scroll
in both directions; and it is a cap, not a length, so a two-thousand-track
queue crosses the bridge as twenty-one entries exactly like a twenty-one-track
one does. Measured at two kilobytes of JSON for twenty-one short titles and
six with long ones and real Android cover paths, against the thirty to eighty
kilobytes of base64 cover that already ride the same call once per record —
and an update is a track change, a transport change or the thirty-second
top-up, never a position tick.
Because it is on no screen, the walk reads it out of `logcat`:
`MediaSessionPlugin.update` logs one `media window n=… i=… art=… at=…` line
per update under the tag `SoulStream`, and the phone walk's `queue-window`
check holds that against `__ss.card` — the mirror `media.svelte.js` keeps of
what it last sent, beside xfernote's — field for field, through a next and a
previous. Field for field is deliberate: a key spelled snake_case where the
bridge wants camelCase arrives as nothing at all on both hops, with no error
anywhere, which is why the art-key count is compared and not just the size and
the index.
Three things in that check are load-bearing and easy to undo by accident. It
arms at queue index **one** rather than wherever the walk left off, which buys
a row on either side (`prev()` seeks to zero at the top of a queue and
`next()` stops at the end), a track that has just started (`prev()` is a seek
again past five seconds), and the fact that while the queue index is at most
ten nothing is clamped off the front of the window — so the window's index IS
the queue index, which is what lets the logged index be compared against the
queue's directly. It requires each reading to be a FRESH one at both ends, the
push counter up and a new line in the log, because a `prev()` that lands back
on the row it started from reads exactly like a stale line otherwise. And it
FAILS rather than skips when it cannot be armed — a queue under three rows, or
repeat-one, under which `next()` seeks instead of stepping — with the reason
in the message.
## What a screen reader says
Every control on the phone is named by what it does, in the app's own
@@ -1851,12 +1895,13 @@ read over real IPC; a cold artist page, its ⋯, the whole-discography page and
its A–Z; the first-run preview with `setupWrites` at 0; eleven raw backend
strings through `__ss.say`; and a refused transfer's sentence.
Then, after the step loop, seven things the mock cannot do at all: a
Then, after the step loop, eight things the mock cannot do at all: a
force-stop and the paused restore that follows it (waited out past the
thirteen-second FGS-kill window), the position restored, playback resumed, the
system media session and its ownership, media keys, ten seconds in the
background with the position still advancing, and SoulStream's own mixer
underruns.
background with the position still advancing, SoulStream's own mixer
underruns, and the queue window that rides along with every card update and
that nothing on the card reads.
One block in the phone walk pulls bytes, and only when the app is signed in:
the trailing network block types `--query` into Search, streams a peer's copy