The same Tauri 2 project builds for Android: mobile.html as a third Vite
entry (src/mobile/, the mockup on real data), tauri.android.conf.json, the
mobile stubs (updater, lyric alignment, vocal separation), rustls reqwest
on Android, the AAudio buffer, and a hand-driven detour build
(src-tauri/android/{env,build,deploy,dev}.sh) while Developer Mode is off.
ANDROID.md is the recipe.
Playback: the media-session plugin crate (lock-screen card, MediaStyle
notification, audio focus, headset buttons, a foreground service that
promotes itself first — Android 14 kills the app otherwise), session
restore across reload and restart, solid chrome after measuring the
backdrop-filter lag.
The catalog: Search is the desktop's top bar (the top-result rule, artist
cards), the artist page and the record page are the desktop's, Home is the
desktop's landing page, Gaps per followed shelf. Playing goes through the
desktop's own source rules, moved verbatim from state.svelte.js into
src/lib/sources.js so both apps share one resolver; the phone readies the
next three tracks and crosses boundaries gapless. Save fetches a record or
a track into the library by the download rules; Transfers shows the
burst. The raw Soulseek search stays, offered on request, a peer's copy
streaming with the lane rising as the bytes arrive. Community is the
Soulseek side (rooms, chat, buddies); the private layer waits.
The opener and storage plugins (social links, the picked library folder,
all-files access), the volume normalization switch, and the harness:
tests/mobile/smoke.mjs walks the mock (a fake network behind
src/lib/events.js) and the real phone over CDP, perf.mjs measures frames.
102 lines
4.6 KiB
JavaScript
102 lines
4.6 KiB
JavaScript
/* The DevTools protocol, for the phone walk and the perf probe.
|
|
|
|
Two things live here: a client small enough to read (a WebSocket, ids,
|
|
a map of waits), and the phone attach — a debug build's WebView exposes
|
|
DevTools on `localabstract:webview_devtools_remote_<pid>`, which
|
|
`adb forward` puts on a local port. The attach refuses to run when
|
|
another app has the phone's screen: the harness never takes the phone
|
|
from whoever is holding it. */
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { MiniWS } from "./miniws.mjs";
|
|
|
|
/* Node 22 has a global WebSocket; Node 19 does not */
|
|
const WS = typeof WebSocket !== "undefined" ? WebSocket : MiniWS;
|
|
|
|
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
export const ADB = [
|
|
path.join(process.env.LOCALAPPDATA || "", "Android/Sdk/platform-tools/adb.exe"),
|
|
path.join(process.env.ANDROID_HOME || "", "platform-tools/adb.exe"),
|
|
"adb",
|
|
].find((p) => p === "adb" || fs.existsSync(p));
|
|
|
|
export function adb(...args) {
|
|
const r = spawnSync(ADB, args, { encoding: "buffer", maxBuffer: 64 * 1024 * 1024 });
|
|
if (r.status !== 0) throw new Error("adb " + args.join(" ") + ": " + (r.stderr || "").toString());
|
|
return r.stdout;
|
|
}
|
|
|
|
export class CDP {
|
|
constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); this.on = {}; }
|
|
static async attach(port, { phone = false } = {}) {
|
|
for (let i = 0; i < 200; i++) {
|
|
try {
|
|
/* a backgrounded WebView accepts the TCP connection and never
|
|
answers — without a timeout this loop would hang for good */
|
|
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(2500) })).json();
|
|
const page = list.find((t) => t.type === "page");
|
|
if (page) {
|
|
const ws = new WS(page.webSocketDebuggerUrl);
|
|
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
|
|
const c = new CDP(ws);
|
|
ws.onmessage = (ev) => {
|
|
const m = JSON.parse(ev.data);
|
|
if (m.id && c.waits.has(m.id)) { c.waits.get(m.id)(m); c.waits.delete(m.id); }
|
|
else if (m.method && c.on[m.method]) c.on[m.method](m.params);
|
|
};
|
|
return c;
|
|
}
|
|
} catch { /* not up yet */ }
|
|
await sleep(200);
|
|
}
|
|
throw new Error("could not attach to the page on port " + port
|
|
+ (phone ? " (a WebView in the background does not answer; is SoulStream on screen?)" : ""));
|
|
}
|
|
send(method, params = {}) {
|
|
const id = ++this.id;
|
|
this.ws.send(JSON.stringify({ id, method, params }));
|
|
return new Promise((res) => this.waits.set(id, res));
|
|
}
|
|
async evaluate(expr) {
|
|
const r = await this.send("Runtime.evaluate", { expression: expr, awaitPromise: true, returnByValue: true });
|
|
if (r.result && r.result.exceptionDetails) {
|
|
throw new Error("page threw: " + (r.result.exceptionDetails.exception?.description || JSON.stringify(r.result.exceptionDetails)));
|
|
}
|
|
return r.result?.result?.value;
|
|
}
|
|
close() { try { this.ws.close(); } catch { /* fine */ } }
|
|
}
|
|
|
|
/** What is on the phone's screen, from the window manager. */
|
|
export function phoneFocus() {
|
|
return (adb("shell", "dumpsys", "window").toString().match(/mCurrentFocus=[^\n]*/) || [""])[0];
|
|
}
|
|
|
|
/** Attach to SoulStream's WebView on the connected phone. Launches the app
|
|
* only from the launcher; refuses when another app is in front. */
|
|
export async function attachPhone(port) {
|
|
const focus = phoneFocus();
|
|
if (!/com\.soulstream\.app/.test(focus)) {
|
|
if (/launcher/i.test(focus)) {
|
|
adb("shell", "am", "start", "-n", "com.soulstream.app/.MainActivity");
|
|
await sleep(2500);
|
|
} else {
|
|
const who = (focus.match(/\s([\w.]+)\/[^}]*\}/) || [])[1] || "another app";
|
|
throw new Error("the phone is in use (" + who + " is on screen); not touching it");
|
|
}
|
|
}
|
|
const pid = adb("shell", "pidof", "com.soulstream.app").toString().trim();
|
|
if (!pid) throw new Error("SoulStream is not running on the phone (adb shell pidof found nothing)");
|
|
adb("forward", "--remove-all");
|
|
adb("forward", `tcp:${port}`, `localabstract:webview_devtools_remote_${pid}`);
|
|
const cdp = await CDP.attach(port, { phone: true });
|
|
return { cdp, pid };
|
|
}
|
|
|
|
/** The phone's own frame, status bar included. */
|
|
export function phoneStill(file) {
|
|
fs.writeFileSync(file, adb("exec-out", "screencap", "-p"));
|
|
}
|