@sqaitech/android-playground 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/bin/server.bin +0 -0
- package/bin/sqai-android-playground +3 -0
- package/dist/es/bin.mjs +470 -0
- package/dist/lib/bin.js +543 -0
- package/dist/types/bin.d.ts +1 -0
- package/package.json +48 -0
- package/static/favicon.ico +0 -0
- package/static/index.html +1 -0
- package/static/static/css/index.ea878c95.css +2 -0
- package/static/static/css/index.ea878c95.css.map +1 -0
- package/static/static/image/sqai-logo.b7f781cd.png +0 -0
- package/static/static/js/374.e8fd2f39.js +650 -0
- package/static/static/js/374.e8fd2f39.js.LICENSE.txt +173 -0
- package/static/static/js/374.e8fd2f39.js.map +1 -0
- package/static/static/js/async/166.834644b5.js +2 -0
- package/static/static/js/async/166.834644b5.js.map +1 -0
- package/static/static/js/async/173.f2381e64.js +3 -0
- package/static/static/js/async/173.f2381e64.js.map +1 -0
- package/static/static/js/async/212.850ade70.js +158 -0
- package/static/static/js/async/212.850ade70.js.map +1 -0
- package/static/static/js/async/329.261bc4a1.js +26 -0
- package/static/static/js/async/329.261bc4a1.js.map +1 -0
- package/static/static/js/async/364.d88c3cff.js +30 -0
- package/static/static/js/async/364.d88c3cff.js.map +1 -0
- package/static/static/js/async/544.18ac9afb.js +2 -0
- package/static/static/js/async/544.18ac9afb.js.map +1 -0
- package/static/static/js/async/582.8f4b5264.js +21 -0
- package/static/static/js/async/582.8f4b5264.js.map +1 -0
- package/static/static/js/async/624.8a1fe2e8.js +3 -0
- package/static/static/js/async/624.8a1fe2e8.js.map +1 -0
- package/static/static/js/async/644.910ce3d0.js +1 -0
- package/static/static/js/async/659.d19e8c15.js +21 -0
- package/static/static/js/async/659.d19e8c15.js.map +1 -0
- package/static/static/js/async/702.1f38a17e.js +231 -0
- package/static/static/js/async/702.1f38a17e.js.map +1 -0
- package/static/static/js/async/920.48d269c8.js +2 -0
- package/static/static/js/async/920.48d269c8.js.map +1 -0
- package/static/static/js/async/983.b98b40af.js +1 -0
- package/static/static/js/index.601ef220.js +10 -0
- package/static/static/js/index.601ef220.js.LICENSE.txt +7 -0
- package/static/static/js/index.601ef220.js.map +1 -0
- package/static/static/js/index.6a22ac98.js +10 -0
- package/static/static/js/index.6a22ac98.js.LICENSE.txt +7 -0
- package/static/static/js/index.6a22ac98.js.map +1 -0
- package/static/static/js/index.7dfa4eb9.js +10 -0
- package/static/static/js/index.7dfa4eb9.js.LICENSE.txt +7 -0
- package/static/static/js/index.7dfa4eb9.js.map +1 -0
- package/static/static/js/index.a6c47344.js +10 -0
- package/static/static/js/index.a6c47344.js.LICENSE.txt +7 -0
- package/static/static/js/index.a6c47344.js.map +1 -0
- package/static/static/js/index.c1b7cd4d.js +10 -0
- package/static/static/js/index.c1b7cd4d.js.LICENSE.txt +7 -0
- package/static/static/js/index.c1b7cd4d.js.map +1 -0
- package/static/static/js/index.c645d278.js +10 -0
- package/static/static/js/index.c645d278.js.LICENSE.txt +7 -0
- package/static/static/js/index.c645d278.js.map +1 -0
- package/static/static/js/index.d16798c4.js +10 -0
- package/static/static/js/index.d16798c4.js.LICENSE.txt +7 -0
- package/static/static/js/index.d16798c4.js.map +1 -0
- package/static/static/js/index.d9ec7a58.js +10 -0
- package/static/static/js/index.d9ec7a58.js.LICENSE.txt +7 -0
- package/static/static/js/index.d9ec7a58.js.map +1 -0
- package/static/static/js/index.f02202b8.js +10 -0
- package/static/static/js/index.f02202b8.js.LICENSE.txt +7 -0
- package/static/static/js/index.f02202b8.js.map +1 -0
- package/static/static/js/index.f5c7fdf6.js +10 -0
- package/static/static/js/index.f5c7fdf6.js.LICENSE.txt +7 -0
- package/static/static/js/index.f5c7fdf6.js.map +1 -0
- package/static/static/js/lib-react.c74a0742.js +3 -0
- package/static/static/js/lib-react.c74a0742.js.LICENSE.txt +39 -0
- package/static/static/js/lib-react.c74a0742.js.map +1 -0
- package/static/static/wasm/9e906fbf55e08f98.module.wasm +0 -0
package/README.md
ADDED
package/bin/server.bin
ADDED
|
Binary file
|
package/dist/es/bin.mjs
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import node_path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { select as prompts_select } from "@inquirer/prompts";
|
|
6
|
+
import { AndroidAgent, AndroidDevice } from "@sqaitech/android";
|
|
7
|
+
import { PlaygroundServer } from "@sqaitech/playground";
|
|
8
|
+
import { PLAYGROUND_SERVER_PORT, SCRCPY_SERVER_PORT } from "@sqaitech/shared/constants";
|
|
9
|
+
import { createReadStream } from "node:fs";
|
|
10
|
+
import { createServer as external_node_http_createServer } from "node:http";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { getDebug } from "@sqaitech/shared/logger";
|
|
13
|
+
import cors from "cors";
|
|
14
|
+
import express from "express";
|
|
15
|
+
import { Server } from "socket.io";
|
|
16
|
+
function _define_property(obj, key, value) {
|
|
17
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
18
|
+
value: value,
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true
|
|
22
|
+
});
|
|
23
|
+
else obj[key] = value;
|
|
24
|
+
return obj;
|
|
25
|
+
}
|
|
26
|
+
const debugPage = getDebug('android:playground');
|
|
27
|
+
const promiseExec = promisify(exec);
|
|
28
|
+
class ScrcpyServer {
|
|
29
|
+
setupApiRoutes() {
|
|
30
|
+
this.app.get('/api/devices', async (req, res)=>{
|
|
31
|
+
try {
|
|
32
|
+
const devices = await this.getDevicesList();
|
|
33
|
+
res.json({
|
|
34
|
+
devices,
|
|
35
|
+
currentDeviceId: this.currentDeviceId
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
res.status(500).json({
|
|
39
|
+
error: error.message || 'Failed to get devices list'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async getDevicesList() {
|
|
45
|
+
try {
|
|
46
|
+
debugPage('start to get devices list');
|
|
47
|
+
const client = await this.getAdbClient();
|
|
48
|
+
if (!client) {
|
|
49
|
+
console.warn('failed to get adb client');
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
debugPage('success to get adb client, start to request devices list');
|
|
53
|
+
let devices;
|
|
54
|
+
try {
|
|
55
|
+
devices = await client.getDevices();
|
|
56
|
+
debugPage('original devices list:', devices);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('failed to get devices list:', error);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
if (!devices || 0 === devices.length) return [];
|
|
62
|
+
const formattedDevices = devices.map((device)=>{
|
|
63
|
+
const result = {
|
|
64
|
+
id: device.serial,
|
|
65
|
+
name: device.product || device.model || device.serial,
|
|
66
|
+
status: device.state || 'device'
|
|
67
|
+
};
|
|
68
|
+
return result;
|
|
69
|
+
});
|
|
70
|
+
return formattedDevices;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('failed to get devices list:', error);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async getAdbClient() {
|
|
77
|
+
const { AdbServerClient } = await import("@yume-chan/adb");
|
|
78
|
+
const { AdbServerNodeTcpConnector } = await import("@yume-chan/adb-server-node-tcp");
|
|
79
|
+
try {
|
|
80
|
+
if (this.adbClient) debugPage('use existing adb client');
|
|
81
|
+
else {
|
|
82
|
+
await promiseExec('adb start-server');
|
|
83
|
+
debugPage('adb server started');
|
|
84
|
+
debugPage('initialize adb client');
|
|
85
|
+
this.adbClient = new AdbServerClient(new AdbServerNodeTcpConnector({
|
|
86
|
+
host: '127.0.0.1',
|
|
87
|
+
port: 5037
|
|
88
|
+
}));
|
|
89
|
+
await debugPage('success to initialize adb client');
|
|
90
|
+
}
|
|
91
|
+
return this.adbClient;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('failed to get adb client:', error);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async getAdb(deviceId) {
|
|
98
|
+
const { Adb } = await import("@yume-chan/adb");
|
|
99
|
+
try {
|
|
100
|
+
const client = await this.getAdbClient();
|
|
101
|
+
if (!client) return null;
|
|
102
|
+
const targetDeviceId = deviceId || this.currentDeviceId;
|
|
103
|
+
if (targetDeviceId) {
|
|
104
|
+
this.currentDeviceId = targetDeviceId;
|
|
105
|
+
return new Adb(await client.createTransport({
|
|
106
|
+
serial: targetDeviceId
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
const devices = await client.getDevices();
|
|
110
|
+
if (0 === devices.length) return null;
|
|
111
|
+
this.currentDeviceId = devices[0].serial;
|
|
112
|
+
return new Adb(await client.createTransport(devices[0]));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('failed to get adb client:', error);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async startScrcpy(adb, options = {}) {
|
|
119
|
+
const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
|
|
120
|
+
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
121
|
+
const { ScrcpyOptions3_1, DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
122
|
+
const currentDir = 'undefined' != typeof __dirname ? __dirname : node_path.dirname(fileURLToPath(import.meta.url));
|
|
123
|
+
const serverBinPath = node_path.resolve(currentDir, '../../bin/server.bin');
|
|
124
|
+
try {
|
|
125
|
+
await AdbScrcpyClient.pushServer(adb, ReadableStream.from(createReadStream(serverBinPath)));
|
|
126
|
+
const scrcpyOptions = new ScrcpyOptions3_1({
|
|
127
|
+
audio: false,
|
|
128
|
+
control: true,
|
|
129
|
+
maxSize: 1024,
|
|
130
|
+
videoBitRate: 2000000,
|
|
131
|
+
...options
|
|
132
|
+
});
|
|
133
|
+
return await AdbScrcpyClient.start(adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error('failed to start scrcpy:', error);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
setupSocketHandlers() {
|
|
140
|
+
this.io.on('connection', async (socket)=>{
|
|
141
|
+
debugPage('client connected, id: %s, client address: %s', socket.id, socket.handshake.address);
|
|
142
|
+
let scrcpyClient = null;
|
|
143
|
+
let adb = null;
|
|
144
|
+
const sendDevicesList = async ()=>{
|
|
145
|
+
try {
|
|
146
|
+
debugPage('Socket request to get devices list');
|
|
147
|
+
const devices = await this.getDevicesList();
|
|
148
|
+
debugPage('send devices list to client:', devices);
|
|
149
|
+
socket.emit('devices-list', {
|
|
150
|
+
devices,
|
|
151
|
+
currentDeviceId: this.currentDeviceId
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('failed to send devices list:', error);
|
|
155
|
+
socket.emit('error', {
|
|
156
|
+
message: 'failed to get devices list'
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
await sendDevicesList();
|
|
161
|
+
socket.on('get-devices', async ()=>{
|
|
162
|
+
debugPage('received client request to get devices list');
|
|
163
|
+
await sendDevicesList();
|
|
164
|
+
});
|
|
165
|
+
socket.on('switch-device', async (deviceId)=>{
|
|
166
|
+
debugPage('received client request to switch device:', deviceId);
|
|
167
|
+
try {
|
|
168
|
+
if (scrcpyClient) {
|
|
169
|
+
await scrcpyClient.close();
|
|
170
|
+
scrcpyClient = null;
|
|
171
|
+
}
|
|
172
|
+
this.currentDeviceId = deviceId;
|
|
173
|
+
debugPage('device switched to:', deviceId);
|
|
174
|
+
socket.emit('device-switched', {
|
|
175
|
+
deviceId
|
|
176
|
+
});
|
|
177
|
+
this.io.emit('global-device-switched', {
|
|
178
|
+
deviceId,
|
|
179
|
+
timestamp: Date.now()
|
|
180
|
+
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('failed to switch device:', error);
|
|
183
|
+
socket.emit('error', {
|
|
184
|
+
message: `Failed to switch device: ${(null == error ? void 0 : error.message) || 'Unknown error'}`
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
socket.on('connect-device', async (options)=>{
|
|
189
|
+
const { ScrcpyVideoCodecId } = await import("@yume-chan/scrcpy");
|
|
190
|
+
try {
|
|
191
|
+
debugPage('received device connection request, options: %s, client id: %s', options, socket.id);
|
|
192
|
+
adb = await this.getAdb(this.currentDeviceId || void 0);
|
|
193
|
+
if (!adb) {
|
|
194
|
+
console.error('no available device found');
|
|
195
|
+
socket.emit('error', {
|
|
196
|
+
message: 'No device found'
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
debugPage('starting scrcpy service, device id: %s', this.currentDeviceId);
|
|
201
|
+
scrcpyClient = await this.startScrcpy(adb, options);
|
|
202
|
+
debugPage('scrcpy service started successfully');
|
|
203
|
+
debugPage('check scrcpyClient object structure: %s', Object.getOwnPropertyNames(scrcpyClient).map((name)=>{
|
|
204
|
+
const type = typeof scrcpyClient[name];
|
|
205
|
+
const isPromise = 'object' === type && scrcpyClient[name] && 'function' == typeof scrcpyClient[name].then;
|
|
206
|
+
return `${name}: ${type}${isPromise ? ' (Promise)' : ''}`;
|
|
207
|
+
}));
|
|
208
|
+
try {
|
|
209
|
+
if (scrcpyClient.videoStream) {
|
|
210
|
+
debugPage('videoStream exists, type: %s', typeof scrcpyClient.videoStream);
|
|
211
|
+
let videoStream;
|
|
212
|
+
if ('object' == typeof scrcpyClient.videoStream && 'function' == typeof scrcpyClient.videoStream.then) {
|
|
213
|
+
debugPage('videoStream is a Promise, waiting for resolution...');
|
|
214
|
+
videoStream = await scrcpyClient.videoStream;
|
|
215
|
+
} else {
|
|
216
|
+
debugPage('videoStream is not a Promise, directly use');
|
|
217
|
+
videoStream = scrcpyClient.videoStream;
|
|
218
|
+
}
|
|
219
|
+
debugPage('video stream fetched successfully, metadata: %s', videoStream.metadata);
|
|
220
|
+
const metadata = videoStream.metadata || {};
|
|
221
|
+
debugPage('original metadata: %s', metadata);
|
|
222
|
+
if (!metadata.codec) {
|
|
223
|
+
debugPage('metadata does not have codec field, use H264 by default');
|
|
224
|
+
metadata.codec = ScrcpyVideoCodecId.H264;
|
|
225
|
+
}
|
|
226
|
+
if (!metadata.width || !metadata.height) {
|
|
227
|
+
debugPage('metadata does not have width or height field, use default values');
|
|
228
|
+
metadata.width = metadata.width || 1080;
|
|
229
|
+
metadata.height = metadata.height || 1920;
|
|
230
|
+
}
|
|
231
|
+
debugPage('prepare to send video-metadata event to client, data: %s', JSON.stringify(metadata));
|
|
232
|
+
socket.emit('video-metadata', metadata);
|
|
233
|
+
debugPage('video-metadata event sent to client, id: %s', socket.id);
|
|
234
|
+
const { stream } = videoStream;
|
|
235
|
+
const reader = stream.getReader();
|
|
236
|
+
const processStream = async ()=>{
|
|
237
|
+
try {
|
|
238
|
+
while(true){
|
|
239
|
+
const { done, value } = await reader.read();
|
|
240
|
+
if (done) break;
|
|
241
|
+
const frameType = value.type || 'data';
|
|
242
|
+
socket.emit('video-data', {
|
|
243
|
+
data: Array.from(value.data),
|
|
244
|
+
type: frameType,
|
|
245
|
+
timestamp: Date.now(),
|
|
246
|
+
keyFrame: value.keyFrame
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error('error processing video stream:', error);
|
|
251
|
+
socket.emit('error', {
|
|
252
|
+
message: 'video stream processing error'
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
processStream();
|
|
257
|
+
} else {
|
|
258
|
+
console.error('scrcpyClient object does not have videoStream property');
|
|
259
|
+
socket.emit('error', {
|
|
260
|
+
message: 'Video stream not available in scrcpy client'
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('error processing video stream:', error);
|
|
265
|
+
socket.emit('error', {
|
|
266
|
+
message: `Video stream processing error: ${error.message}`
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (null == scrcpyClient ? void 0 : scrcpyClient.controller) socket.emit('control-ready');
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error('failed to connect device:', error);
|
|
272
|
+
socket.emit('error', {
|
|
273
|
+
message: `Failed to connect device: ${(null == error ? void 0 : error.message) || 'Unknown error'}`
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
socket.on('disconnect', async (reason)=>{
|
|
278
|
+
debugPage('client disconnected, id: %s, reason: %s', socket.id, reason);
|
|
279
|
+
if (scrcpyClient) {
|
|
280
|
+
try {
|
|
281
|
+
debugPage('closing scrcpy client');
|
|
282
|
+
await scrcpyClient.close();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('failed to close scrcpy client:', error);
|
|
285
|
+
}
|
|
286
|
+
scrcpyClient = null;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async launch(port) {
|
|
292
|
+
this.port = port || this.defaultPort;
|
|
293
|
+
return new Promise((resolve)=>{
|
|
294
|
+
this.httpServer.listen(this.port, ()=>{
|
|
295
|
+
console.log(`Scrcpy server running at: http://localhost:${this.port}`);
|
|
296
|
+
this.startDeviceMonitoring();
|
|
297
|
+
resolve(this);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
startDeviceMonitoring() {
|
|
302
|
+
this.devicePollInterval = setInterval(async ()=>{
|
|
303
|
+
try {
|
|
304
|
+
const devices = await this.getDevicesList();
|
|
305
|
+
const currentDevicesJson = JSON.stringify(devices);
|
|
306
|
+
if (this.lastDeviceList !== currentDevicesJson) {
|
|
307
|
+
debugPage('devices list changed, push to all connected clients');
|
|
308
|
+
this.lastDeviceList = currentDevicesJson;
|
|
309
|
+
if (!this.currentDeviceId && devices.length > 0) {
|
|
310
|
+
const onlineDevices = devices.filter((device)=>'device' === device.status.toLowerCase());
|
|
311
|
+
if (onlineDevices.length > 0) {
|
|
312
|
+
this.currentDeviceId = onlineDevices[0].id;
|
|
313
|
+
debugPage('auto select the first online device:', this.currentDeviceId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
this.io.emit('devices-list', {
|
|
317
|
+
devices,
|
|
318
|
+
currentDeviceId: this.currentDeviceId
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('device monitoring error:', error);
|
|
323
|
+
}
|
|
324
|
+
}, 3000);
|
|
325
|
+
}
|
|
326
|
+
close() {
|
|
327
|
+
if (this.devicePollInterval) {
|
|
328
|
+
clearInterval(this.devicePollInterval);
|
|
329
|
+
this.devicePollInterval = null;
|
|
330
|
+
}
|
|
331
|
+
if (this.httpServer) return this.httpServer.close();
|
|
332
|
+
}
|
|
333
|
+
constructor(){
|
|
334
|
+
_define_property(this, "app", void 0);
|
|
335
|
+
_define_property(this, "httpServer", void 0);
|
|
336
|
+
_define_property(this, "io", void 0);
|
|
337
|
+
_define_property(this, "port", void 0);
|
|
338
|
+
_define_property(this, "defaultPort", SCRCPY_SERVER_PORT);
|
|
339
|
+
_define_property(this, "adbClient", null);
|
|
340
|
+
_define_property(this, "currentDeviceId", null);
|
|
341
|
+
_define_property(this, "devicePollInterval", null);
|
|
342
|
+
_define_property(this, "lastDeviceList", '');
|
|
343
|
+
this.app = express();
|
|
344
|
+
this.httpServer = external_node_http_createServer(this.app);
|
|
345
|
+
this.io = new Server(this.httpServer, {
|
|
346
|
+
cors: {
|
|
347
|
+
origin: [
|
|
348
|
+
/^http:\/\/localhost(:\d+)?$/,
|
|
349
|
+
/^http:\/\/127\.0\.0\.1(:\d+)?$/
|
|
350
|
+
],
|
|
351
|
+
methods: [
|
|
352
|
+
'GET',
|
|
353
|
+
'POST'
|
|
354
|
+
],
|
|
355
|
+
credentials: true
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
this.app.use(cors({
|
|
359
|
+
origin: '*',
|
|
360
|
+
credentials: true
|
|
361
|
+
}));
|
|
362
|
+
this.setupSocketHandlers();
|
|
363
|
+
this.setupApiRoutes();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const bin_promiseExec = promisify(exec);
|
|
367
|
+
async function isPortAvailable(port) {
|
|
368
|
+
return new Promise((resolve)=>{
|
|
369
|
+
const server = createServer();
|
|
370
|
+
server.on('error', ()=>resolve(false));
|
|
371
|
+
server.listen(port, ()=>{
|
|
372
|
+
server.close(()=>resolve(true));
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
async function findAvailablePort(startPort) {
|
|
377
|
+
let port = startPort;
|
|
378
|
+
let attempts = 0;
|
|
379
|
+
const maxAttempts = 15;
|
|
380
|
+
while(!await isPortAvailable(port)){
|
|
381
|
+
attempts++;
|
|
382
|
+
if (attempts >= maxAttempts) {
|
|
383
|
+
console.error(`\u{274C} Unable to find available port after ${maxAttempts} attempts starting from ${startPort}`);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
port++;
|
|
387
|
+
}
|
|
388
|
+
return port;
|
|
389
|
+
}
|
|
390
|
+
async function getAdbDevices() {
|
|
391
|
+
try {
|
|
392
|
+
await bin_promiseExec('adb start-server');
|
|
393
|
+
const { stdout } = await bin_promiseExec('adb devices');
|
|
394
|
+
const lines = stdout.trim().split('\n').slice(1);
|
|
395
|
+
const devices = lines.map((line)=>{
|
|
396
|
+
const parts = line.trim().split('\t');
|
|
397
|
+
if (parts.length >= 2) return {
|
|
398
|
+
id: parts[0],
|
|
399
|
+
status: parts[1],
|
|
400
|
+
name: parts[0]
|
|
401
|
+
};
|
|
402
|
+
return null;
|
|
403
|
+
}).filter((device)=>null !== device && 'device' === device.status);
|
|
404
|
+
return devices;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error('Error getting ADB devices:', error);
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function selectDevice() {
|
|
411
|
+
console.log("\uD83D\uDD0D Scanning for Android devices...");
|
|
412
|
+
const devices = await getAdbDevices();
|
|
413
|
+
if (0 === devices.length) {
|
|
414
|
+
console.error("\u274C No Android devices found!");
|
|
415
|
+
console.log("\uD83D\uDCF1 Please ensure:");
|
|
416
|
+
console.log(" \u2022 Your device is connected via USB");
|
|
417
|
+
console.log(" \u2022 USB debugging is enabled");
|
|
418
|
+
console.log(" \u2022 Device is authorized for debugging");
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
if (1 === devices.length) {
|
|
422
|
+
console.log(`\u{1F4F1} Found device: ${devices[0].name} (${devices[0].id})`);
|
|
423
|
+
return devices[0].id;
|
|
424
|
+
}
|
|
425
|
+
const choices = devices.map((device)=>({
|
|
426
|
+
name: `${device.name} (${device.id})`,
|
|
427
|
+
value: device.id
|
|
428
|
+
}));
|
|
429
|
+
const selectedDevice = await prompts_select({
|
|
430
|
+
message: "\uD83D\uDCF1 Multiple devices found. Please select one:",
|
|
431
|
+
choices
|
|
432
|
+
});
|
|
433
|
+
return selectedDevice;
|
|
434
|
+
}
|
|
435
|
+
const staticDir = node_path.join(__dirname, '../../static');
|
|
436
|
+
const main = async ()=>{
|
|
437
|
+
const { default: open } = await import("open");
|
|
438
|
+
try {
|
|
439
|
+
const selectedDeviceId = await selectDevice();
|
|
440
|
+
console.log(`\u{2705} Selected device: ${selectedDeviceId}`);
|
|
441
|
+
const playgroundServer = new PlaygroundServer(async ()=>{
|
|
442
|
+
const device = new AndroidDevice(selectedDeviceId);
|
|
443
|
+
await device.connect();
|
|
444
|
+
return new AndroidAgent(device);
|
|
445
|
+
}, staticDir);
|
|
446
|
+
const scrcpyServer = new ScrcpyServer();
|
|
447
|
+
scrcpyServer.currentDeviceId = selectedDeviceId;
|
|
448
|
+
console.log("\uD83D\uDE80 Starting servers...");
|
|
449
|
+
const availablePlaygroundPort = await findAvailablePort(PLAYGROUND_SERVER_PORT);
|
|
450
|
+
const availableScrcpyPort = await findAvailablePort(SCRCPY_SERVER_PORT);
|
|
451
|
+
if (availablePlaygroundPort !== PLAYGROUND_SERVER_PORT) console.log(`\u{26A0}\u{FE0F} Port ${PLAYGROUND_SERVER_PORT} is busy, using port ${availablePlaygroundPort} instead`);
|
|
452
|
+
if (availableScrcpyPort !== SCRCPY_SERVER_PORT) console.log(`\u{26A0}\u{FE0F} Port ${SCRCPY_SERVER_PORT} is busy, using port ${availableScrcpyPort} instead`);
|
|
453
|
+
await Promise.all([
|
|
454
|
+
playgroundServer.launch(availablePlaygroundPort),
|
|
455
|
+
scrcpyServer.launch(availableScrcpyPort)
|
|
456
|
+
]);
|
|
457
|
+
global.scrcpyServerPort = availableScrcpyPort;
|
|
458
|
+
console.log('');
|
|
459
|
+
console.log("\u2728 Midscene Android Playground is ready!");
|
|
460
|
+
console.log(`\u{1F3AE} Playground: http://localhost:${playgroundServer.port}`);
|
|
461
|
+
console.log(`\u{1F4F1} Device: ${selectedDeviceId}`);
|
|
462
|
+
console.log(`\u{1F511} Generated Server ID: ${playgroundServer.id}`);
|
|
463
|
+
console.log('');
|
|
464
|
+
open(`http://localhost:${playgroundServer.port}`);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.error('Failed to start servers:', error);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
main();
|