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:
1 parent
d42645d2a0
commit
afcd3cc7f3
7 files changed
+301
-7
No files matched your search
@@ -2485,6 +2485,23 @@ function mediaSessionState() {
|
||||
} catch { return { owner: "", state: "" }; }
|
||||
}
|
||||
|
||||
/* The queue window the phone was last handed, as MediaSessionPlugin logged
|
||||
* it. Round forty-six put a bounded slice of the queue into every card
|
||||
* update — the current item and ten either side — and left MediaService
|
||||
* ignoring all of it, so there is nothing on any screen to read it back
|
||||
* from: logcat is the only place on the device it exists, one line per
|
||||
* update, and the LAST line is the state now. `at=` is last in the line
|
||||
* because a title may hold anything, spaces and `=` included. */
|
||||
function mediaWindow() {
|
||||
try {
|
||||
const out = adb("logcat", "-d", "-s", "SoulStream:D").toString();
|
||||
const lines = out.split(/\r?\n/).filter((l) => l.includes("media window "));
|
||||
const m = (lines[lines.length - 1] || "").match(/media window n=(\d+) i=(-?\d+) art=(\d+) at=(.*)$/);
|
||||
if (!m) return { n: -1, i: -1, art: -1, at: "", lines: lines.length };
|
||||
return { n: +m[1], i: +m[2], art: +m[3], at: m[4].trim(), lines: lines.length };
|
||||
} catch (e) { return { n: -1, i: -1, art: -1, at: "ERR " + e.message, lines: 0 }; }
|
||||
}
|
||||
|
||||
/* ── the phone, turned over: the two settings that do it ───────────────
|
||||
* `settings get system accelerometer_rotation` and `user_rotation` are the
|
||||
* only two things on the device this walk ever writes, and both are READ
|
||||
@@ -3003,6 +3020,90 @@ async function main() {
|
||||
if (!keysOk) problems.push("EXPECT media-keys " + JSON.stringify({ keyPaused, keyPlaying }));
|
||||
console.log((keysOk ? " ok " : " FAIL ") + "media-keys".padEnd(16) + " " + JSON.stringify({ paused: keyPaused?.status, playing: keyPlaying?.status }));
|
||||
|
||||
/* ── the queue window, across the bridge ───────────────────
|
||||
Round forty-six put a bounded window of the queue into every
|
||||
card update — the current item and ten either side, each with
|
||||
its id, title, artist, album, duration and art key, plus where
|
||||
the current one sits among them — and left MediaService
|
||||
ignoring every bit of it, because media3's
|
||||
SimpleBasePlayer.getState() will want a playlist and an index,
|
||||
and that data should be a round old before anything depends on
|
||||
it. Which means every check above this one is blind to it: a
|
||||
window that arrived empty, an index off by one, or a field the
|
||||
bridge dropped because a key was spelled the wrong way would
|
||||
each look exactly like success.
|
||||
|
||||
So: what the phone SENT — __ss.card, the mirror
|
||||
media.svelte.js keeps beside xfernote's — against what the
|
||||
plugin LOGGED on the far side, field for field, through a next
|
||||
and a previous. Field for field is the point: a camelCase key
|
||||
spelled snake_case arrives as nothing at all on both hops of
|
||||
this bridge, with no error anywhere, so `art` (the only
|
||||
camelCase field an entry has) is compared as well as the size
|
||||
and the index.
|
||||
|
||||
ARMED TO A KNOWN ROW, index one, rather than to wherever the
|
||||
walk left off. Three things fall out of that and every one of
|
||||
them is load-bearing: there is a row on either side, so next()
|
||||
steps instead of stopping at the end and prev() steps instead
|
||||
of seeking to zero at the top; the track starts at zero, so
|
||||
prev() is still a step two and a half seconds later rather
|
||||
than the seek it becomes past five, and no track ends
|
||||
underneath the three readings; and while the queue index is
|
||||
under ten the window's own index IS the queue index, because
|
||||
the slice starts at zero and nothing is clamped off the front
|
||||
— which is what makes `got.i` comparable to `base` below
|
||||
without recomputing the arithmetic here. Nothing is added,
|
||||
removed or reordered: this is the queue the walk has been
|
||||
playing all along, stepped one row forward and one row back
|
||||
inside itself.
|
||||
|
||||
Not skipped when it cannot be armed: a queue shorter than
|
||||
three rows, or repeat-one — under which next() seeks to zero
|
||||
instead of stepping — FAILS, with `armed` in the message
|
||||
saying which. A check that passes by not running is the thing
|
||||
this walk is most careful never to be. */
|
||||
const CARD = `(function () { var s = window.__ss; if (!s || !s.card || !s.player) return null; var c = s.card, p = s.player; return { u: c.updates, n: c.window, i: c.index, art: c.art, at: c.at, qi: p.qi, title: p.track ? p.track.title : '' }; })()`;
|
||||
const armed = await cdp.evaluate(`(function () { var p = window.__ss && __ss.player; return p ? { n: p.queue.length, qi: p.qi, rep: p.repeat } : null; })()`);
|
||||
const base = 1;
|
||||
const room = (armed?.n ?? 0) >= 3 && armed.rep !== 2;
|
||||
const wins = [];
|
||||
if (room) {
|
||||
await cdp.evaluate(`__ss.playQueueAt(${base})`);
|
||||
await sleep(2500);
|
||||
wins.push(["here", await cdp.evaluate(CARD), mediaWindow()]);
|
||||
await cdp.evaluate(`__ss.next()`);
|
||||
await sleep(2500);
|
||||
wins.push(["next", await cdp.evaluate(CARD), mediaWindow()]);
|
||||
await cdp.evaluate(`__ss.prev()`);
|
||||
await sleep(2500);
|
||||
wins.push(["prev", await cdp.evaluate(CARD), mediaWindow()]);
|
||||
}
|
||||
/* each reading: the window SENT is a real one — at least two
|
||||
entries, the index inside it, and the entry AT the index is the
|
||||
track on the bar, which is what an off-by-one fails even when
|
||||
the counts agree — and what the plugin LOGGED is that same
|
||||
window, field for field. */
|
||||
const sane = ([, sent, got]) => !!sent && sent.n >= 2 && sent.i >= 0 && sent.i < sent.n
|
||||
&& !!sent.at && sent.at === sent.title
|
||||
&& got.n === sent.n && got.i === sent.i && got.art === sent.art
|
||||
&& got.at === String(sent.at).trim();
|
||||
const winOk = room && wins.length === 3 && wins.every(sane)
|
||||
/* and it MOVED. The queue index steps forward and back; the
|
||||
LOGGED window index steps with it, which is the assertion
|
||||
an off-by-one or a frozen index dies on. And each reading
|
||||
is a FRESH one at both ends — the phone pushed again
|
||||
(`u`) and the plugin logged again (`lines`) — because a
|
||||
prev() that lands back on the row it started from reads
|
||||
identically to a stale line otherwise. */
|
||||
&& wins[0][1].qi === base && wins[1][1].qi === base + 1 && wins[2][1].qi === base
|
||||
&& wins[0][2].i === base && wins[1][2].i === base + 1 && wins[2][2].i === base
|
||||
&& wins[1][1].u > wins[0][1].u && wins[2][1].u > wins[1][1].u
|
||||
&& wins[1][2].lines > wins[0][2].lines && wins[2][2].lines > wins[1][2].lines;
|
||||
if (!winOk) problems.push("EXPECT queue-window " + JSON.stringify({ armed, base, wins }));
|
||||
console.log((winOk ? " ok " : " FAIL ") + "queue-window".padEnd(16) + " "
|
||||
+ JSON.stringify({ armed, seen: wins.map(([k, sent, got]) => [k, sent && sent.qi, got.n, got.i, got.art, got.at]) }));
|
||||
|
||||
const bgBefore = await cdp.evaluate(PLAYER);
|
||||
const posBefore = bgBefore?.pos ?? 0;
|
||||
cdp.close();
|
||||
|
||||
Reference in new issue
Block a user