Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/android-playground/src/scrcpy-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Server as HttpServer } from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { installAdbServerClientTransportIdFeatures } from '@midscene/android/internal/adb-server-client-transport-id-features';
import {
SCRCPY_ADB_CONNECT_TIMEOUT_MS,
SCRCPY_PREVIEW_METADATA_TIMEOUT_MS,
Expand Down Expand Up @@ -240,6 +241,7 @@ export default class ScrcpyServer {
port: 5037,
}),
);
installAdbServerClientTransportIdFeatures(this.adbClient);
await debugPage('success to initialize adb client');
} else {
debugPage('use existing adb client');
Expand Down
71 changes: 64 additions & 7 deletions packages/android-playground/tests/unit/scrcpy-server.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import ScrcpyServer, {
resolveRequestedDeviceId,
} from '../../src/scrcpy-server';
Expand All @@ -9,12 +9,46 @@ const {
mockReadableFrom,
mockCreateReadStream,
mockOptionsCtor,
} = vi.hoisted(() => ({
mockPushServer: vi.fn(),
mockStart: vi.fn(),
mockReadableFrom: vi.fn(),
mockCreateReadStream: vi.fn(),
mockOptionsCtor: vi.fn((options) => options),
mockExec,
mockInstallTransportIdFeatures,
mockAdbClient,
mockAdbServerClient,
mockAdbServerNodeTcpConnector,
} = vi.hoisted(() => {
const mockAdbClient = {};

return {
mockPushServer: vi.fn(),
mockStart: vi.fn(),
mockReadableFrom: vi.fn(),
mockCreateReadStream: vi.fn(),
mockOptionsCtor: vi.fn((options) => options),
mockExec: vi.fn((_command, callback) => callback(null, '', '')),
mockInstallTransportIdFeatures: vi.fn(),
mockAdbClient,
mockAdbServerClient: vi.fn().mockImplementation(() => mockAdbClient),
mockAdbServerNodeTcpConnector: vi.fn(),
};
});

vi.mock('node:child_process', () => ({
exec: mockExec,
}));

vi.mock(
'@midscene/android/internal/adb-server-client-transport-id-features',
() => ({
installAdbServerClientTransportIdFeatures: mockInstallTransportIdFeatures,
}),
);

vi.mock('@yume-chan/adb', () => ({
Adb: vi.fn().mockImplementation(() => ({})),
AdbServerClient: mockAdbServerClient,
}));

vi.mock('@yume-chan/adb-server-node-tcp', () => ({
AdbServerNodeTcpConnector: mockAdbServerNodeTcpConnector,
}));

vi.mock('@yume-chan/adb-scrcpy', () => ({
Expand Down Expand Up @@ -44,6 +78,10 @@ vi.mock('node:fs', async (importOriginal) => {
});

describe('ScrcpyServer', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('prefers the explicit device from the preview handshake', () => {
expect(
resolveRequestedDeviceId(
Expand Down Expand Up @@ -91,6 +129,25 @@ describe('ScrcpyServer', () => {
]);
});

it('installs transport-id features handling when initializing the ADB client', async () => {
const server = new ScrcpyServer();

await expect((server as any).getAdbClient()).resolves.toBe(mockAdbClient);

expect(mockExec).toHaveBeenCalledWith(
'adb start-server',
expect.any(Function),
);
expect(mockAdbServerNodeTcpConnector).toHaveBeenCalledWith({
host: '127.0.0.1',
port: 5037,
});
expect(mockAdbServerClient).toHaveBeenCalledTimes(1);
expect(mockInstallTransportIdFeatures).toHaveBeenCalledWith(mockAdbClient);

server.close();
});

it('can consume device list updates from an external discovery source', async () => {
const unsubscribe = vi.fn();
const getDevices = vi.fn().mockResolvedValue([
Expand Down
5 changes: 5 additions & 0 deletions packages/android/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
"import": "./dist/es/mcp-server.mjs",
"require": "./dist/lib/mcp-server.js"
},
"./internal/adb-server-client-transport-id-features": {
"types": "./dist/types/internal/adb-server-client-transport-id-features.d.ts",
"import": "./dist/es/internal/adb-server-client-transport-id-features.mjs",
"require": "./dist/lib/internal/adb-server-client-transport-id-features.js"
},
"./package.json": "./package.json"
},
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions packages/android/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export default defineConfig({
index: './src/index.ts',
cli: './src/cli.ts',
'mcp-server': './src/mcp-server.ts',
'internal/adb-server-client-transport-id-features':
'./src/internal/adb-server-client-transport-id-features.ts',
},
define: {
__VERSION__: JSON.stringify(version),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
type AdbFeature,
AdbServerClient,
type AdbServerClient as AdbServerClientType,
} from '@yume-chan/adb';

type DeviceFeatures = {
transportId: bigint;
features: readonly AdbFeature[];
};

const patchedClients = new WeakSet<AdbServerClientType>();

async function resolveDeterministicTransportId(
client: AdbServerClientType,
device: AdbServerClient.DeviceSelector,
): Promise<bigint | undefined> {
if (device && 'transportId' in device) {
return device.transportId;
}

if (device && 'serial' in device) {
const devices = await client.getDevices();
return devices.find((info) => info.serial === device.serial)?.transportId;
}

return undefined;
}

async function getDeviceFeaturesByTransportId(
client: AdbServerClientType,
transportId: bigint,
): Promise<DeviceFeatures> {
const connection = await client.createConnection(
AdbServerClient.formatDeviceService({ transportId }, 'features'),
);
try {
const featuresString = await connection.readString();
const features = featuresString
? (featuresString.split(',') as AdbFeature[])
: [];
return { transportId, features };
} finally {
await connection.dispose();
}
}

/**
* @internal Prefer transport-id qualified feature requests for deterministic
* selectors before yume-chan/adb includes this behavior.
*/
export function installAdbServerClientTransportIdFeatures(
client: AdbServerClientType,
): void {
if (patchedClients.has(client)) {
return;
}

const getDeviceFeatures = client.getDeviceFeatures.bind(client);
client.getDeviceFeatures = async (device) => {
let transportId: bigint | undefined;
try {
transportId = await resolveDeterministicTransportId(client, device);
} catch {
return getDeviceFeatures(device);
}

if (transportId !== undefined) {
return getDeviceFeaturesByTransportId(client, transportId);
}

return getDeviceFeatures(device);
};

patchedClients.add(client);
}
2 changes: 2 additions & 0 deletions packages/android/src/scrcpy-device-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Size } from '@midscene/core';
import { createImgBase64ByFormat } from '@midscene/shared/img';
import { getDebug } from '@midscene/shared/logger';
import { installAdbServerClientTransportIdFeatures } from './internal/adb-server-client-transport-id-features';
import type { ScrcpyScreenshotManager } from './scrcpy-manager';
import { DEFAULT_SCRCPY_CONFIG } from './scrcpy-manager';

Expand Down Expand Up @@ -111,6 +112,7 @@ export class ScrcpyDeviceAdapter {
const adbClient = new AdbServerClient(
new AdbServerNodeTcpConnector({ host: '127.0.0.1', port: 5037 }),
);
installAdbServerClientTransportIdFeatures(adbClient);
const adb = new Adb(
await adbClient.createTransport({ serial: this.deviceId }),
);
Expand Down
Loading
Loading