consortium
Version:
Remote control and session sharing CLI for AI coding agents
66 lines (57 loc) • 3.2 kB
JavaScript
// Standalone scrcpy smoke test — proves the exact @yume-chan calls the
// Android backend's startScrcpy needs: connect → push server → H.264 video
// stream → inject a tap. Run against a booted emulator on this box.
import { AdbServerClient, Adb } from '@yume-chan/adb';
import { AdbServerNodeTcpConnector } from '@yume-chan/adb-server-node-tcp';
import { AdbScrcpyClient, AdbScrcpyOptions2_1 } from '@yume-chan/adb-scrcpy';
import { DefaultServerPath, AndroidMotionEventAction } from '@yume-chan/scrcpy';
import { ReadableStream } from '@yume-chan/stream-extra';
import { readFileSync } from 'node:fs';
const log = (...a) => console.log('[smoke]', ...a);
const connector = new AdbServerNodeTcpConnector({ host: '127.0.0.1', port: 5037 });
const server = new AdbServerClient(connector);
const devices = await server.getDevices();
log('devices:', devices.map((d) => d.serial));
const device = devices.find((d) => d.serial === 'emulator-5554') ?? devices[0];
if (!device) throw new Error('no adb device');
const transport = await server.createTransport(device);
const adb = new Adb(transport);
log('connected to', device.serial);
const jar = readFileSync(new URL('./scrcpy-server', import.meta.url));
await AdbScrcpyClient.pushServer(
adb,
new ReadableStream({ start(c) { c.enqueue(new Uint8Array(jar)); c.close(); } }),
);
log('pushed scrcpy-server', jar.length, 'bytes');
const options = new AdbScrcpyOptions2_1({ video: true, audio: false, control: true, maxSize: 1024, maxFps: 15 });
const client = await AdbScrcpyClient.start(adb, DefaultServerPath, options);
log('scrcpy started');
const video = await client.videoStream;
log('video metadata:', JSON.stringify(video.metadata), 'size', video.width + 'x' + video.height);
let dataPackets = 0, configPackets = 0, keyframes = 0, totalBytes = 0;
const reader = video.stream.getReader();
const deadline = Date.now() + 8000;
while (Date.now() < deadline && dataPackets < 40) {
const { done, value } = await reader.read();
if (done) break;
if (value.type === 'configuration') { configPackets++; continue; }
dataPackets++; totalBytes += value.data.byteLength;
if (value.keyframe) keyframes++;
if (dataPackets <= 3) log(` frame#${dataPackets} keyframe=${!!value.keyframe} bytes=${value.data.byteLength}`);
}
reader.releaseLock();
log(`VIDEO: ${dataPackets} data packets, ${configPackets} config, ${keyframes} keyframes, ${totalBytes} bytes total`);
// Inject a tap at screen center (down + up).
const w = video.width, h = video.height, cx = Math.floor(w / 2), cy = Math.floor(h / 2);
const controller = client.controller;
if (!controller) throw new Error('no controller (control disabled?)');
const touch = (action, pressure) => controller.injectTouch({
pointerId: 0n, action, pointerX: cx, pointerY: cy, screenWidth: w, screenHeight: h,
pressure, actionButton: 0, buttons: 0,
});
await touch(AndroidMotionEventAction.Down, 1);
await touch(AndroidMotionEventAction.Up, 0);
log(`TAP injected at ${cx},${cy} on ${w}x${h}`);
log(dataPackets > 0 && keyframes > 0 ? 'SMOKE PASS ✅' : 'SMOKE FAIL ❌ (no frames/keyframe)');
await client.close?.();
process.exit(dataPackets > 0 && keyframes > 0 ? 0 : 1);