@midscene/android-playground 0.14.4-beta-20250416024415.0 → 0.14.4-beta-20250416041002.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.
@@ -0,0 +1,460 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/index.ts
26
+ var import_node_path = __toESM(require("path"));
27
+ var import_android = require("@midscene/android");
28
+ var import_constants2 = require("@midscene/shared/constants");
29
+ var import_midscene_server = __toESM(require("@midscene/web/midscene-server"));
30
+
31
+ // src/scrcpy-server.ts
32
+ var import_node_child_process = require("child_process");
33
+ var import_node_fs = require("fs");
34
+ var import_node_http = require("http");
35
+ var import_node_util = require("util");
36
+ var import_constants = require("@midscene/shared/constants");
37
+ var import_logger = require("@midscene/shared/logger");
38
+ var import_cors = __toESM(require("cors"));
39
+ var import_express = __toESM(require("express"));
40
+ var import_socket = require("socket.io");
41
+ var debugPage = (0, import_logger.getDebug)("android:playground");
42
+ var promiseExec = (0, import_node_util.promisify)(import_node_child_process.exec);
43
+ var ScrcpyServer = class {
44
+ // use for comparing changes
45
+ constructor() {
46
+ this.defaultPort = import_constants.SCRCPY_SERVER_PORT;
47
+ this.adbClient = null;
48
+ this.currentDeviceId = null;
49
+ this.devicePollInterval = null;
50
+ this.lastDeviceList = "";
51
+ this.app = (0, import_express.default)();
52
+ this.httpServer = (0, import_node_http.createServer)(this.app);
53
+ this.io = new import_socket.Server(this.httpServer, {
54
+ cors: {
55
+ origin: [
56
+ /^http:\/\/localhost(:\d+)?$/,
57
+ /^http:\/\/127\.0\.0\.1(:\d+)?$/
58
+ ],
59
+ methods: ["GET", "POST"],
60
+ credentials: true
61
+ }
62
+ });
63
+ this.app.use(
64
+ (0, import_cors.default)({
65
+ origin: "*",
66
+ credentials: true
67
+ })
68
+ );
69
+ this.setupSocketHandlers();
70
+ this.setupApiRoutes();
71
+ }
72
+ // setup API routes
73
+ setupApiRoutes() {
74
+ this.app.get("/api/devices", async (req, res) => {
75
+ try {
76
+ const devices = await this.getDevicesList();
77
+ res.json({ devices, currentDeviceId: this.currentDeviceId });
78
+ } catch (error) {
79
+ res.status(500).json({ error: error.message || "Failed to get devices list" });
80
+ }
81
+ });
82
+ }
83
+ // get devices list
84
+ async getDevicesList() {
85
+ try {
86
+ debugPage("start to get devices list");
87
+ const client = await this.getAdbClient();
88
+ if (!client) {
89
+ console.warn("failed to get adb client");
90
+ return [];
91
+ }
92
+ debugPage("success to get adb client, start to request devices list");
93
+ let devices;
94
+ try {
95
+ devices = await client.getDevices();
96
+ debugPage("original devices list:", devices);
97
+ } catch (error) {
98
+ console.error("failed to get devices list:", error);
99
+ return [];
100
+ }
101
+ if (!devices || devices.length === 0) {
102
+ return [];
103
+ }
104
+ const formattedDevices = devices.map((device) => {
105
+ const result = {
106
+ id: device.serial,
107
+ name: device.product || device.model || device.serial,
108
+ status: device.state || "device"
109
+ };
110
+ return result;
111
+ });
112
+ return formattedDevices;
113
+ } catch (error) {
114
+ console.error("failed to get devices list:", error);
115
+ return [];
116
+ }
117
+ }
118
+ // get adb client
119
+ async getAdbClient() {
120
+ const { AdbServerClient } = await import("@yume-chan/adb");
121
+ const { AdbServerNodeTcpConnector } = await import("@yume-chan/adb-server-node-tcp");
122
+ try {
123
+ if (!this.adbClient) {
124
+ await promiseExec("adb start-server");
125
+ debugPage("adb server started");
126
+ debugPage("initialize adb client");
127
+ this.adbClient = new AdbServerClient(
128
+ new AdbServerNodeTcpConnector({
129
+ host: "localhost",
130
+ port: 5037
131
+ })
132
+ );
133
+ await debugPage("success to initialize adb client");
134
+ } else {
135
+ debugPage("use existing adb client");
136
+ }
137
+ return this.adbClient;
138
+ } catch (error) {
139
+ console.error("failed to get adb client:", error);
140
+ return null;
141
+ }
142
+ }
143
+ // get adb object
144
+ async getAdb(deviceId) {
145
+ const { Adb } = await import("@yume-chan/adb");
146
+ try {
147
+ const client = await this.getAdbClient();
148
+ if (!client) {
149
+ return null;
150
+ }
151
+ if (deviceId) {
152
+ this.currentDeviceId = deviceId;
153
+ return new Adb(await client.createTransport({ serial: deviceId }));
154
+ }
155
+ const devices = await client.getDevices();
156
+ if (devices.length === 0) {
157
+ return null;
158
+ }
159
+ this.currentDeviceId = devices[0].serial;
160
+ return new Adb(await client.createTransport(devices[0]));
161
+ } catch (error) {
162
+ console.error("failed to get adb client:", error);
163
+ return null;
164
+ }
165
+ }
166
+ // start scrcpy
167
+ async startScrcpy(adb, options = {}) {
168
+ const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
169
+ const { ReadableStream } = await import("@yume-chan/stream-extra");
170
+ const { ScrcpyOptions3_1, DefaultServerPath } = await import("@yume-chan/scrcpy");
171
+ const { BIN } = await import("@yume-chan/fetch-scrcpy-server");
172
+ try {
173
+ await AdbScrcpyClient.pushServer(
174
+ adb,
175
+ ReadableStream.from((0, import_node_fs.createReadStream)(BIN))
176
+ );
177
+ const scrcpyOptions = new ScrcpyOptions3_1({
178
+ // default options
179
+ audio: false,
180
+ control: true,
181
+ maxSize: 1024,
182
+ // use videoBitRate as property name
183
+ videoBitRate: 2e6,
184
+ // override default values with user provided options
185
+ ...options
186
+ });
187
+ return await AdbScrcpyClient.start(
188
+ adb,
189
+ DefaultServerPath,
190
+ new AdbScrcpyOptions2_1(scrcpyOptions)
191
+ );
192
+ } catch (error) {
193
+ console.error("failed to start scrcpy:", error);
194
+ throw error;
195
+ }
196
+ }
197
+ // setup Socket.IO connection handlers
198
+ setupSocketHandlers() {
199
+ this.io.on("connection", async (socket) => {
200
+ debugPage(
201
+ "client connected, id: %s, client address: %s",
202
+ socket.id,
203
+ socket.handshake.address
204
+ );
205
+ let scrcpyClient = null;
206
+ let adb = null;
207
+ const sendDevicesList = async () => {
208
+ try {
209
+ debugPage("Socket request to get devices list");
210
+ const devices = await this.getDevicesList();
211
+ debugPage("send devices list to client:", devices);
212
+ socket.emit("devices-list", {
213
+ devices,
214
+ currentDeviceId: this.currentDeviceId
215
+ });
216
+ } catch (error) {
217
+ console.error("failed to send devices list:", error);
218
+ socket.emit("error", { message: "failed to get devices list" });
219
+ }
220
+ };
221
+ await sendDevicesList();
222
+ socket.on("get-devices", async () => {
223
+ debugPage("received client request to get devices list");
224
+ await sendDevicesList();
225
+ });
226
+ socket.on("switch-device", async (deviceId) => {
227
+ debugPage("received client request to switch device:", deviceId);
228
+ try {
229
+ if (scrcpyClient) {
230
+ await scrcpyClient.close();
231
+ scrcpyClient = null;
232
+ }
233
+ this.currentDeviceId = deviceId;
234
+ debugPage("device switched to:", deviceId);
235
+ socket.emit("device-switched", { deviceId });
236
+ this.io.emit("global-device-switched", {
237
+ deviceId,
238
+ timestamp: Date.now()
239
+ });
240
+ } catch (error) {
241
+ console.error("failed to switch device:", error);
242
+ socket.emit("error", {
243
+ message: `Failed to switch device: ${error?.message || "Unknown error"}`
244
+ });
245
+ }
246
+ });
247
+ socket.on("connect-device", async (options) => {
248
+ const { ScrcpyVideoCodecId } = await import("@yume-chan/scrcpy");
249
+ try {
250
+ debugPage(
251
+ "received device connection request, options: %s, client id: %s",
252
+ options,
253
+ socket.id
254
+ );
255
+ adb = await this.getAdb(this.currentDeviceId || void 0);
256
+ if (!adb) {
257
+ console.error("no available device found");
258
+ socket.emit("error", { message: "No device found" });
259
+ return;
260
+ }
261
+ debugPage(
262
+ "starting scrcpy service, device id: %s",
263
+ this.currentDeviceId
264
+ );
265
+ scrcpyClient = await this.startScrcpy(adb, options);
266
+ debugPage("scrcpy service started successfully");
267
+ debugPage(
268
+ "check scrcpyClient object structure: %s",
269
+ Object.getOwnPropertyNames(scrcpyClient).map((name) => {
270
+ const type = typeof scrcpyClient[name];
271
+ const isPromise = type === "object" && scrcpyClient[name] && typeof scrcpyClient[name].then === "function";
272
+ return `${name}: ${type}${isPromise ? " (Promise)" : ""}`;
273
+ })
274
+ );
275
+ try {
276
+ if (scrcpyClient.videoStream) {
277
+ debugPage(
278
+ "videoStream exists, type: %s",
279
+ typeof scrcpyClient.videoStream
280
+ );
281
+ let videoStream;
282
+ if (typeof scrcpyClient.videoStream === "object" && typeof scrcpyClient.videoStream.then === "function") {
283
+ debugPage(
284
+ "videoStream is a Promise, waiting for resolution..."
285
+ );
286
+ videoStream = await scrcpyClient.videoStream;
287
+ } else {
288
+ debugPage("videoStream is not a Promise, directly use");
289
+ videoStream = scrcpyClient.videoStream;
290
+ }
291
+ debugPage(
292
+ "video stream fetched successfully, metadata: %s",
293
+ videoStream.metadata
294
+ );
295
+ const metadata = videoStream.metadata || {};
296
+ debugPage("original metadata: %s", metadata);
297
+ if (!metadata.codec) {
298
+ debugPage(
299
+ "metadata does not have codec field, use H264 by default"
300
+ );
301
+ metadata.codec = ScrcpyVideoCodecId.H264;
302
+ }
303
+ if (!metadata.width || !metadata.height) {
304
+ debugPage(
305
+ "metadata does not have width or height field, use default values"
306
+ );
307
+ metadata.width = metadata.width || 1080;
308
+ metadata.height = metadata.height || 1920;
309
+ }
310
+ debugPage(
311
+ "prepare to send video-metadata event to client, data: %s",
312
+ JSON.stringify(metadata)
313
+ );
314
+ socket.emit("video-metadata", metadata);
315
+ debugPage(
316
+ "video-metadata event sent to client, id: %s",
317
+ socket.id
318
+ );
319
+ const { stream } = videoStream;
320
+ const reader = stream.getReader();
321
+ const processStream = async () => {
322
+ try {
323
+ while (true) {
324
+ const { done, value } = await reader.read();
325
+ if (done)
326
+ break;
327
+ const frameType = value.type || "data";
328
+ socket.emit("video-data", {
329
+ data: Array.from(value.data),
330
+ type: frameType,
331
+ timestamp: Date.now(),
332
+ // fix keyframe access
333
+ keyFrame: value.keyFrame
334
+ });
335
+ }
336
+ } catch (error) {
337
+ console.error("error processing video stream:", error);
338
+ socket.emit("error", {
339
+ message: "video stream processing error"
340
+ });
341
+ }
342
+ };
343
+ processStream();
344
+ } else {
345
+ console.error(
346
+ "scrcpyClient object does not have videoStream property"
347
+ );
348
+ socket.emit("error", {
349
+ message: "Video stream not available in scrcpy client"
350
+ });
351
+ }
352
+ } catch (error) {
353
+ console.error("error processing video stream:", error);
354
+ socket.emit("error", {
355
+ message: `Video stream processing error: ${error.message}`
356
+ });
357
+ }
358
+ if (scrcpyClient?.controller) {
359
+ socket.emit("control-ready");
360
+ }
361
+ } catch (error) {
362
+ console.error("failed to connect device:", error);
363
+ socket.emit("error", {
364
+ message: `Failed to connect device: ${error?.message || "Unknown error"}`
365
+ });
366
+ }
367
+ });
368
+ socket.on("disconnect", async (reason) => {
369
+ debugPage("client disconnected, id: %s, reason: %s", socket.id, reason);
370
+ if (scrcpyClient) {
371
+ try {
372
+ debugPage("closing scrcpy client");
373
+ await scrcpyClient.close();
374
+ } catch (error) {
375
+ console.error("failed to close scrcpy client:", error);
376
+ }
377
+ scrcpyClient = null;
378
+ }
379
+ });
380
+ });
381
+ }
382
+ // launch server
383
+ async launch(port) {
384
+ this.port = port || this.defaultPort;
385
+ return new Promise((resolve) => {
386
+ this.httpServer.listen(this.port, () => {
387
+ console.log(`Scrcpy server running at: http://localhost:${this.port}`);
388
+ this.startDeviceMonitoring();
389
+ resolve(this);
390
+ });
391
+ });
392
+ }
393
+ // start device monitoring
394
+ startDeviceMonitoring() {
395
+ this.devicePollInterval = setInterval(async () => {
396
+ try {
397
+ const devices = await this.getDevicesList();
398
+ const currentDevicesJson = JSON.stringify(devices);
399
+ if (this.lastDeviceList !== currentDevicesJson) {
400
+ debugPage("devices list changed, push to all connected clients");
401
+ this.lastDeviceList = currentDevicesJson;
402
+ if (!this.currentDeviceId && devices.length > 0) {
403
+ const onlineDevices = devices.filter(
404
+ (device) => device.status.toLowerCase() === "device"
405
+ );
406
+ if (onlineDevices.length > 0) {
407
+ this.currentDeviceId = onlineDevices[0].id;
408
+ debugPage(
409
+ "auto select the first online device:",
410
+ this.currentDeviceId
411
+ );
412
+ }
413
+ }
414
+ this.io.emit("devices-list", {
415
+ devices,
416
+ currentDeviceId: this.currentDeviceId
417
+ });
418
+ }
419
+ } catch (error) {
420
+ console.error("device monitoring error:", error);
421
+ }
422
+ }, 3e3);
423
+ }
424
+ // close server
425
+ close() {
426
+ if (this.devicePollInterval) {
427
+ clearInterval(this.devicePollInterval);
428
+ this.devicePollInterval = null;
429
+ }
430
+ if (this.httpServer) {
431
+ return this.httpServer.close();
432
+ }
433
+ }
434
+ };
435
+
436
+ // src/index.ts
437
+ var staticDir = import_node_path.default.join(__dirname, "../../static");
438
+ var playgroundServer = new import_midscene_server.default(
439
+ import_android.AndroidDevice,
440
+ import_android.AndroidAgent,
441
+ staticDir
442
+ );
443
+ var scrcpyServer = new ScrcpyServer();
444
+ var main = async () => {
445
+ const { default: open } = await import("open");
446
+ try {
447
+ await Promise.all([
448
+ playgroundServer.launch(import_constants2.PLAYGROUND_SERVER_PORT),
449
+ scrcpyServer.launch(import_constants2.SCRCPY_SERVER_PORT)
450
+ ]);
451
+ console.log(
452
+ `Midscene playground server is running on http://localhost:${playgroundServer.port}`
453
+ );
454
+ open(`http://localhost:${playgroundServer.port}`);
455
+ } catch (error) {
456
+ console.error("Failed to start servers:", error);
457
+ process.exit(1);
458
+ }
459
+ };
460
+ main();
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/android-playground",
3
- "version": "0.14.4-beta-20250416024415.0",
3
+ "version": "0.14.4-beta-20250416041002.0",
4
4
  "description": "Android playground for Midscene",
5
5
  "main": "./dist/lib/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -25,9 +25,9 @@
25
25
  "express": "^4.21.2",
26
26
  "open": "10.1.0",
27
27
  "socket.io": "^4.8.1",
28
- "@midscene/shared": "0.14.4-beta-20250416024415.0",
29
- "@midscene/web": "0.14.4-beta-20250416024415.0",
30
- "@midscene/android": "0.14.4-beta-20250416024415.0"
28
+ "@midscene/shared": "0.14.4-beta-20250416041002.0",
29
+ "@midscene/android": "0.14.4-beta-20250416041002.0",
30
+ "@midscene/web": "0.14.4-beta-20250416041002.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@modern-js/module-tools": "2.60.6",
package/static/index.html CHANGED
@@ -1 +1 @@
1
- <!doctype html><html><head><title>Midscene Android Playground</title><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><script defer src="/static/js/lib-react.9955fef2.js"></script><script defer src="/static/js/507.b955f683.js"></script><script defer src="/static/js/index.6a9903a5.js"></script><link href="/static/css/index.21e939b0.css" rel="stylesheet"></head><body><div id="root"></div></body></html>
1
+ <!doctype html><html><head><title>Midscene Android Playground</title><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><script defer src="/static/js/lib-react.9955fef2.js"></script><script defer src="/static/js/507.b955f683.js"></script><script defer src="/static/js/index.809dee96.js"></script><link href="/static/css/index.21e939b0.css" rel="stylesheet"></head><body><div id="root"></div></body></html>