@midscene/android-playground 1.7.3 → 1.7.4
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/dist/es/bin.mjs +103 -18
- package/dist/es/index.mjs +103 -18
- package/dist/lib/bin.js +102 -17
- package/dist/lib/index.js +102 -17
- package/dist/types/scrcpy-preview-status.d.ts +7 -0
- package/dist/types/timeout.d.ts +10 -0
- package/package.json +5 -5
- package/static/index.html +1 -1
- package/static/static/css/index.9808a5ca.css +2 -0
- package/static/static/css/index.9808a5ca.css.map +1 -0
- package/static/static/js/{537.e15c28d4.js → 603.1304125f.js} +52 -63
- package/static/static/js/{537.e15c28d4.js.LICENSE.txt → 603.1304125f.js.LICENSE.txt} +2 -2
- package/static/static/js/603.1304125f.js.map +1 -0
- package/static/static/js/index.5b455c7f.js +896 -0
- package/static/static/js/index.5b455c7f.js.map +1 -0
- package/static/static/css/index.022a8122.css +0 -2
- package/static/static/css/index.022a8122.css.map +0 -1
- package/static/static/js/537.e15c28d4.js.map +0 -1
- package/static/static/js/index.36c459a0.js +0 -896
- package/static/static/js/index.36c459a0.js.map +0 -1
- /package/static/static/js/{index.36c459a0.js.LICENSE.txt → index.5b455c7f.js.LICENSE.txt} +0 -0
package/dist/es/bin.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import node_path from "node:path";
|
|
2
2
|
import { createScrcpyPreviewDescriptor, definePlaygroundPlatform, launchPreparedPlaygroundPlatform } from "@midscene/playground";
|
|
3
3
|
import { AndroidAgent, AndroidDevice, getConnectedDevicesWithDetails } from "@midscene/android";
|
|
4
|
-
import { PLAYGROUND_SERVER_PORT, SCRCPY_SERVER_PORT } from "@midscene/shared/constants";
|
|
4
|
+
import { PLAYGROUND_SERVER_PORT, SCRCPY_PREVIEW_METADATA_TIMEOUT_MS, SCRCPY_PUSH_TIMEOUT_MS, SCRCPY_SERVER_PORT, SCRCPY_START_TIMEOUT_MS, SCRCPY_VIDEO_STREAM_TIMEOUT_MS } from "@midscene/shared/constants";
|
|
5
5
|
import { findAvailablePort } from "@midscene/shared/node";
|
|
6
6
|
import { exec } from "node:child_process";
|
|
7
7
|
import { createReadStream } from "node:fs";
|
|
@@ -42,8 +42,8 @@ const androidPlaygroundPlatform = definePlaygroundPlatform({
|
|
|
42
42
|
async getSetupSchema () {
|
|
43
43
|
const targets = await getAdbTargets();
|
|
44
44
|
return {
|
|
45
|
-
title: '
|
|
46
|
-
description: 'Select an available ADB device to create the current Android Agent
|
|
45
|
+
title: 'Welcome to\nMidscene.js Playground!',
|
|
46
|
+
description: 'Select an available ADB device to create the current Android Agent',
|
|
47
47
|
primaryActionLabel: 'Create Agent',
|
|
48
48
|
autoSubmitWhenReady: 1 === targets.length,
|
|
49
49
|
fields: [
|
|
@@ -129,6 +129,24 @@ const androidPlaygroundPlatform = definePlaygroundPlatform({
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
});
|
|
132
|
+
function getScrcpyPreviewStatusMessage(phase) {
|
|
133
|
+
switch(phase){
|
|
134
|
+
case 'connecting-device':
|
|
135
|
+
return 'Preparing Android device connection…';
|
|
136
|
+
case 'pushing-server':
|
|
137
|
+
return 'Uploading scrcpy runtime to device…';
|
|
138
|
+
case 'starting-service':
|
|
139
|
+
return 'Starting scrcpy service…';
|
|
140
|
+
case 'waiting-for-video':
|
|
141
|
+
return 'Waiting for first video frame…';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function buildScrcpyPreviewStatusEvent(phase) {
|
|
145
|
+
return {
|
|
146
|
+
phase,
|
|
147
|
+
message: getScrcpyPreviewStatusMessage(phase)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
132
150
|
function _define_property(obj, key, value) {
|
|
133
151
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
134
152
|
value: value,
|
|
@@ -139,6 +157,48 @@ function _define_property(obj, key, value) {
|
|
|
139
157
|
else obj[key] = value;
|
|
140
158
|
return obj;
|
|
141
159
|
}
|
|
160
|
+
class PromiseTimeoutError extends Error {
|
|
161
|
+
constructor(message, timeoutMs){
|
|
162
|
+
super(message), _define_property(this, "timeoutMs", void 0);
|
|
163
|
+
this.name = 'PromiseTimeoutError';
|
|
164
|
+
this.timeoutMs = timeoutMs;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function runTimeoutCallback(callback, context) {
|
|
168
|
+
if (!callback) return;
|
|
169
|
+
Promise.resolve().then(callback).catch((error)=>{
|
|
170
|
+
console.error(`Failed to run ${context}:`, error);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function withTimeout(promise, timeoutMs, message, options = {}) {
|
|
174
|
+
return new Promise((resolve, reject)=>{
|
|
175
|
+
let timedOut = false;
|
|
176
|
+
const timer = setTimeout(()=>{
|
|
177
|
+
timedOut = true;
|
|
178
|
+
runTimeoutCallback(options.onTimeout, 'timeout callback');
|
|
179
|
+
reject(new PromiseTimeoutError(message, timeoutMs));
|
|
180
|
+
}, timeoutMs);
|
|
181
|
+
Promise.resolve(promise).then((value)=>{
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
if (timedOut) return void runTimeoutCallback(()=>options.onSettledAfterTimeout?.(value), 'post-timeout settle callback');
|
|
184
|
+
resolve(value);
|
|
185
|
+
}, (error)=>{
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
if (timedOut) return void runTimeoutCallback(()=>options.onRejectedAfterTimeout?.(error), 'post-timeout rejection callback');
|
|
188
|
+
reject(error);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function scrcpy_server_define_property(obj, key, value) {
|
|
193
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
194
|
+
value: value,
|
|
195
|
+
enumerable: true,
|
|
196
|
+
configurable: true,
|
|
197
|
+
writable: true
|
|
198
|
+
});
|
|
199
|
+
else obj[key] = value;
|
|
200
|
+
return obj;
|
|
201
|
+
}
|
|
142
202
|
const debugPage = getDebug('android:playground');
|
|
143
203
|
const promiseExec = promisify(exec);
|
|
144
204
|
const LOOPBACK_HOSTS = new Set([
|
|
@@ -249,14 +309,15 @@ class ScrcpyServer {
|
|
|
249
309
|
return null;
|
|
250
310
|
}
|
|
251
311
|
}
|
|
252
|
-
async startScrcpy(adb, options = {}) {
|
|
312
|
+
async startScrcpy(adb, options = {}, onProgress) {
|
|
253
313
|
const { AdbScrcpyClient, AdbScrcpyOptions3_3_3 } = await import("@yume-chan/adb-scrcpy");
|
|
254
314
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
255
315
|
const { DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
256
316
|
const currentDir = 'undefined' != typeof __dirname ? __dirname : node_path.dirname(fileURLToPath(import.meta.url));
|
|
257
317
|
const serverBinPath = node_path.resolve(currentDir, '../../bin/scrcpy-server');
|
|
258
318
|
try {
|
|
259
|
-
|
|
319
|
+
onProgress?.('pushing-server');
|
|
320
|
+
await withTimeout(AdbScrcpyClient.pushServer(adb, ReadableStream.from(createReadStream(serverBinPath))), SCRCPY_PUSH_TIMEOUT_MS, `Timed out pushing scrcpy server to device after ${Math.round(SCRCPY_PUSH_TIMEOUT_MS / 1000)}s`);
|
|
260
321
|
const scrcpyOptions = new AdbScrcpyOptions3_3_3({
|
|
261
322
|
audio: false,
|
|
262
323
|
control: true,
|
|
@@ -265,7 +326,17 @@ class ScrcpyServer {
|
|
|
265
326
|
videoBitRate: 2000000,
|
|
266
327
|
...options
|
|
267
328
|
});
|
|
268
|
-
|
|
329
|
+
onProgress?.('starting-service');
|
|
330
|
+
const startPromise = AdbScrcpyClient.start(adb, DefaultServerPath, scrcpyOptions);
|
|
331
|
+
return await withTimeout(startPromise, SCRCPY_START_TIMEOUT_MS, `Timed out starting scrcpy service after ${Math.round(SCRCPY_START_TIMEOUT_MS / 1000)}s`, {
|
|
332
|
+
onSettledAfterTimeout: async (lateClient)=>{
|
|
333
|
+
try {
|
|
334
|
+
await lateClient.close();
|
|
335
|
+
} catch (closeError) {
|
|
336
|
+
console.error('failed to close late scrcpy client after timeout:', closeError);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
269
340
|
} catch (error) {
|
|
270
341
|
console.error('failed to start scrcpy:', error);
|
|
271
342
|
throw error;
|
|
@@ -276,6 +347,9 @@ class ScrcpyServer {
|
|
|
276
347
|
debugPage('client connected, id: %s, client address: %s', socket.id, socket.handshake.address);
|
|
277
348
|
let scrcpyClient = null;
|
|
278
349
|
let adb = null;
|
|
350
|
+
const emitPreviewStatus = (phase)=>{
|
|
351
|
+
socket.emit('preview-status', buildScrcpyPreviewStatusEvent(phase));
|
|
352
|
+
};
|
|
279
353
|
const sendDevicesList = async ()=>{
|
|
280
354
|
try {
|
|
281
355
|
debugPage('Socket request to get devices list');
|
|
@@ -324,6 +398,7 @@ class ScrcpyServer {
|
|
|
324
398
|
const { ScrcpyVideoCodecId } = await import("@yume-chan/scrcpy");
|
|
325
399
|
try {
|
|
326
400
|
debugPage('received device connection request, options: %s, client id: %s', options, socket.id);
|
|
401
|
+
emitPreviewStatus('connecting-device');
|
|
327
402
|
adb = await this.getAdb(this.currentDeviceId || void 0);
|
|
328
403
|
if (!adb) {
|
|
329
404
|
console.error('no available device found');
|
|
@@ -333,7 +408,7 @@ class ScrcpyServer {
|
|
|
333
408
|
return;
|
|
334
409
|
}
|
|
335
410
|
debugPage('starting scrcpy service, device id: %s', this.currentDeviceId);
|
|
336
|
-
scrcpyClient = await this.startScrcpy(adb, options);
|
|
411
|
+
scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
337
412
|
debugPage('scrcpy service started successfully');
|
|
338
413
|
debugPage('check scrcpyClient object structure: %s', Object.getOwnPropertyNames(scrcpyClient).map((name)=>{
|
|
339
414
|
const type = typeof scrcpyClient[name];
|
|
@@ -346,9 +421,11 @@ class ScrcpyServer {
|
|
|
346
421
|
let videoStream;
|
|
347
422
|
if ('object' == typeof scrcpyClient.videoStream && 'function' == typeof scrcpyClient.videoStream.then) {
|
|
348
423
|
debugPage('videoStream is a Promise, waiting for resolution...');
|
|
349
|
-
|
|
424
|
+
emitPreviewStatus('waiting-for-video');
|
|
425
|
+
videoStream = await withTimeout(scrcpyClient.videoStream, SCRCPY_VIDEO_STREAM_TIMEOUT_MS, `Timed out waiting for scrcpy video stream metadata after ${Math.round(SCRCPY_VIDEO_STREAM_TIMEOUT_MS / 1000)}s`);
|
|
350
426
|
} else {
|
|
351
427
|
debugPage('videoStream is not a Promise, directly use');
|
|
428
|
+
emitPreviewStatus('waiting-for-video');
|
|
352
429
|
videoStream = scrcpyClient.videoStream;
|
|
353
430
|
}
|
|
354
431
|
debugPage('video stream fetched successfully, metadata: %s', videoStream.metadata);
|
|
@@ -365,7 +442,7 @@ class ScrcpyServer {
|
|
|
365
442
|
}
|
|
366
443
|
debugPage('prepare to send video-metadata event to client, data: %s', JSON.stringify(metadata));
|
|
367
444
|
socket.emit('video-metadata', metadata);
|
|
368
|
-
debugPage('video-metadata event sent to client, id: %s', socket.id);
|
|
445
|
+
debugPage('video-metadata event sent to client, id: %s, timeout budget: %ss', socket.id, Math.round(SCRCPY_PREVIEW_METADATA_TIMEOUT_MS / 1000));
|
|
369
446
|
const { stream } = videoStream;
|
|
370
447
|
const reader = stream.getReader();
|
|
371
448
|
const processStream = async ()=>{
|
|
@@ -404,6 +481,14 @@ class ScrcpyServer {
|
|
|
404
481
|
if (scrcpyClient?.controller) socket.emit('control-ready');
|
|
405
482
|
} catch (error) {
|
|
406
483
|
console.error('failed to connect device:', error);
|
|
484
|
+
if (scrcpyClient) {
|
|
485
|
+
try {
|
|
486
|
+
await scrcpyClient.close();
|
|
487
|
+
} catch (closeError) {
|
|
488
|
+
console.error('failed to close scrcpy client after error:', closeError);
|
|
489
|
+
}
|
|
490
|
+
scrcpyClient = null;
|
|
491
|
+
}
|
|
407
492
|
socket.emit('error', {
|
|
408
493
|
message: `Failed to connect device: ${error?.message || 'Unknown error'}`
|
|
409
494
|
});
|
|
@@ -467,15 +552,15 @@ class ScrcpyServer {
|
|
|
467
552
|
if (this.httpServer) return this.httpServer.close();
|
|
468
553
|
}
|
|
469
554
|
constructor(){
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
555
|
+
scrcpy_server_define_property(this, "app", void 0);
|
|
556
|
+
scrcpy_server_define_property(this, "httpServer", void 0);
|
|
557
|
+
scrcpy_server_define_property(this, "io", void 0);
|
|
558
|
+
scrcpy_server_define_property(this, "port", void 0);
|
|
559
|
+
scrcpy_server_define_property(this, "defaultPort", SCRCPY_SERVER_PORT);
|
|
560
|
+
scrcpy_server_define_property(this, "adbClient", null);
|
|
561
|
+
scrcpy_server_define_property(this, "currentDeviceId", null);
|
|
562
|
+
scrcpy_server_define_property(this, "devicePollInterval", null);
|
|
563
|
+
scrcpy_server_define_property(this, "lastDeviceList", '');
|
|
479
564
|
this.app = express();
|
|
480
565
|
this.httpServer = createServer(this.app);
|
|
481
566
|
this.io = new Server(this.httpServer, {
|
package/dist/es/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import node_path from "node:path";
|
|
2
2
|
import { AndroidAgent, AndroidDevice, getConnectedDevicesWithDetails } from "@midscene/android";
|
|
3
3
|
import { createScrcpyPreviewDescriptor, definePlaygroundPlatform } from "@midscene/playground";
|
|
4
|
-
import { PLAYGROUND_SERVER_PORT, SCRCPY_SERVER_PORT } from "@midscene/shared/constants";
|
|
4
|
+
import { PLAYGROUND_SERVER_PORT, SCRCPY_PREVIEW_METADATA_TIMEOUT_MS, SCRCPY_PUSH_TIMEOUT_MS, SCRCPY_SERVER_PORT, SCRCPY_START_TIMEOUT_MS, SCRCPY_VIDEO_STREAM_TIMEOUT_MS } from "@midscene/shared/constants";
|
|
5
5
|
import { findAvailablePort } from "@midscene/shared/node";
|
|
6
6
|
import { exec } from "node:child_process";
|
|
7
7
|
import { createReadStream } from "node:fs";
|
|
@@ -42,8 +42,8 @@ const androidPlaygroundPlatform = definePlaygroundPlatform({
|
|
|
42
42
|
async getSetupSchema () {
|
|
43
43
|
const targets = await getAdbTargets();
|
|
44
44
|
return {
|
|
45
|
-
title: '
|
|
46
|
-
description: 'Select an available ADB device to create the current Android Agent
|
|
45
|
+
title: 'Welcome to\nMidscene.js Playground!',
|
|
46
|
+
description: 'Select an available ADB device to create the current Android Agent',
|
|
47
47
|
primaryActionLabel: 'Create Agent',
|
|
48
48
|
autoSubmitWhenReady: 1 === targets.length,
|
|
49
49
|
fields: [
|
|
@@ -129,6 +129,24 @@ const androidPlaygroundPlatform = definePlaygroundPlatform({
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
});
|
|
132
|
+
function getScrcpyPreviewStatusMessage(phase) {
|
|
133
|
+
switch(phase){
|
|
134
|
+
case 'connecting-device':
|
|
135
|
+
return 'Preparing Android device connection…';
|
|
136
|
+
case 'pushing-server':
|
|
137
|
+
return 'Uploading scrcpy runtime to device…';
|
|
138
|
+
case 'starting-service':
|
|
139
|
+
return 'Starting scrcpy service…';
|
|
140
|
+
case 'waiting-for-video':
|
|
141
|
+
return 'Waiting for first video frame…';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function buildScrcpyPreviewStatusEvent(phase) {
|
|
145
|
+
return {
|
|
146
|
+
phase,
|
|
147
|
+
message: getScrcpyPreviewStatusMessage(phase)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
132
150
|
function _define_property(obj, key, value) {
|
|
133
151
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
134
152
|
value: value,
|
|
@@ -139,6 +157,48 @@ function _define_property(obj, key, value) {
|
|
|
139
157
|
else obj[key] = value;
|
|
140
158
|
return obj;
|
|
141
159
|
}
|
|
160
|
+
class PromiseTimeoutError extends Error {
|
|
161
|
+
constructor(message, timeoutMs){
|
|
162
|
+
super(message), _define_property(this, "timeoutMs", void 0);
|
|
163
|
+
this.name = 'PromiseTimeoutError';
|
|
164
|
+
this.timeoutMs = timeoutMs;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function runTimeoutCallback(callback, context) {
|
|
168
|
+
if (!callback) return;
|
|
169
|
+
Promise.resolve().then(callback).catch((error)=>{
|
|
170
|
+
console.error(`Failed to run ${context}:`, error);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function withTimeout(promise, timeoutMs, message, options = {}) {
|
|
174
|
+
return new Promise((resolve, reject)=>{
|
|
175
|
+
let timedOut = false;
|
|
176
|
+
const timer = setTimeout(()=>{
|
|
177
|
+
timedOut = true;
|
|
178
|
+
runTimeoutCallback(options.onTimeout, 'timeout callback');
|
|
179
|
+
reject(new PromiseTimeoutError(message, timeoutMs));
|
|
180
|
+
}, timeoutMs);
|
|
181
|
+
Promise.resolve(promise).then((value)=>{
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
if (timedOut) return void runTimeoutCallback(()=>options.onSettledAfterTimeout?.(value), 'post-timeout settle callback');
|
|
184
|
+
resolve(value);
|
|
185
|
+
}, (error)=>{
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
if (timedOut) return void runTimeoutCallback(()=>options.onRejectedAfterTimeout?.(error), 'post-timeout rejection callback');
|
|
188
|
+
reject(error);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function scrcpy_server_define_property(obj, key, value) {
|
|
193
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
194
|
+
value: value,
|
|
195
|
+
enumerable: true,
|
|
196
|
+
configurable: true,
|
|
197
|
+
writable: true
|
|
198
|
+
});
|
|
199
|
+
else obj[key] = value;
|
|
200
|
+
return obj;
|
|
201
|
+
}
|
|
142
202
|
const debugPage = getDebug('android:playground');
|
|
143
203
|
const promiseExec = promisify(exec);
|
|
144
204
|
const LOOPBACK_HOSTS = new Set([
|
|
@@ -249,14 +309,15 @@ class ScrcpyServer {
|
|
|
249
309
|
return null;
|
|
250
310
|
}
|
|
251
311
|
}
|
|
252
|
-
async startScrcpy(adb, options = {}) {
|
|
312
|
+
async startScrcpy(adb, options = {}, onProgress) {
|
|
253
313
|
const { AdbScrcpyClient, AdbScrcpyOptions3_3_3 } = await import("@yume-chan/adb-scrcpy");
|
|
254
314
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
255
315
|
const { DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
256
316
|
const currentDir = 'undefined' != typeof __dirname ? __dirname : node_path.dirname(fileURLToPath(import.meta.url));
|
|
257
317
|
const serverBinPath = node_path.resolve(currentDir, '../../bin/scrcpy-server');
|
|
258
318
|
try {
|
|
259
|
-
|
|
319
|
+
onProgress?.('pushing-server');
|
|
320
|
+
await withTimeout(AdbScrcpyClient.pushServer(adb, ReadableStream.from(createReadStream(serverBinPath))), SCRCPY_PUSH_TIMEOUT_MS, `Timed out pushing scrcpy server to device after ${Math.round(SCRCPY_PUSH_TIMEOUT_MS / 1000)}s`);
|
|
260
321
|
const scrcpyOptions = new AdbScrcpyOptions3_3_3({
|
|
261
322
|
audio: false,
|
|
262
323
|
control: true,
|
|
@@ -265,7 +326,17 @@ class ScrcpyServer {
|
|
|
265
326
|
videoBitRate: 2000000,
|
|
266
327
|
...options
|
|
267
328
|
});
|
|
268
|
-
|
|
329
|
+
onProgress?.('starting-service');
|
|
330
|
+
const startPromise = AdbScrcpyClient.start(adb, DefaultServerPath, scrcpyOptions);
|
|
331
|
+
return await withTimeout(startPromise, SCRCPY_START_TIMEOUT_MS, `Timed out starting scrcpy service after ${Math.round(SCRCPY_START_TIMEOUT_MS / 1000)}s`, {
|
|
332
|
+
onSettledAfterTimeout: async (lateClient)=>{
|
|
333
|
+
try {
|
|
334
|
+
await lateClient.close();
|
|
335
|
+
} catch (closeError) {
|
|
336
|
+
console.error('failed to close late scrcpy client after timeout:', closeError);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
269
340
|
} catch (error) {
|
|
270
341
|
console.error('failed to start scrcpy:', error);
|
|
271
342
|
throw error;
|
|
@@ -276,6 +347,9 @@ class ScrcpyServer {
|
|
|
276
347
|
debugPage('client connected, id: %s, client address: %s', socket.id, socket.handshake.address);
|
|
277
348
|
let scrcpyClient = null;
|
|
278
349
|
let adb = null;
|
|
350
|
+
const emitPreviewStatus = (phase)=>{
|
|
351
|
+
socket.emit('preview-status', buildScrcpyPreviewStatusEvent(phase));
|
|
352
|
+
};
|
|
279
353
|
const sendDevicesList = async ()=>{
|
|
280
354
|
try {
|
|
281
355
|
debugPage('Socket request to get devices list');
|
|
@@ -324,6 +398,7 @@ class ScrcpyServer {
|
|
|
324
398
|
const { ScrcpyVideoCodecId } = await import("@yume-chan/scrcpy");
|
|
325
399
|
try {
|
|
326
400
|
debugPage('received device connection request, options: %s, client id: %s', options, socket.id);
|
|
401
|
+
emitPreviewStatus('connecting-device');
|
|
327
402
|
adb = await this.getAdb(this.currentDeviceId || void 0);
|
|
328
403
|
if (!adb) {
|
|
329
404
|
console.error('no available device found');
|
|
@@ -333,7 +408,7 @@ class ScrcpyServer {
|
|
|
333
408
|
return;
|
|
334
409
|
}
|
|
335
410
|
debugPage('starting scrcpy service, device id: %s', this.currentDeviceId);
|
|
336
|
-
scrcpyClient = await this.startScrcpy(adb, options);
|
|
411
|
+
scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
337
412
|
debugPage('scrcpy service started successfully');
|
|
338
413
|
debugPage('check scrcpyClient object structure: %s', Object.getOwnPropertyNames(scrcpyClient).map((name)=>{
|
|
339
414
|
const type = typeof scrcpyClient[name];
|
|
@@ -346,9 +421,11 @@ class ScrcpyServer {
|
|
|
346
421
|
let videoStream;
|
|
347
422
|
if ('object' == typeof scrcpyClient.videoStream && 'function' == typeof scrcpyClient.videoStream.then) {
|
|
348
423
|
debugPage('videoStream is a Promise, waiting for resolution...');
|
|
349
|
-
|
|
424
|
+
emitPreviewStatus('waiting-for-video');
|
|
425
|
+
videoStream = await withTimeout(scrcpyClient.videoStream, SCRCPY_VIDEO_STREAM_TIMEOUT_MS, `Timed out waiting for scrcpy video stream metadata after ${Math.round(SCRCPY_VIDEO_STREAM_TIMEOUT_MS / 1000)}s`);
|
|
350
426
|
} else {
|
|
351
427
|
debugPage('videoStream is not a Promise, directly use');
|
|
428
|
+
emitPreviewStatus('waiting-for-video');
|
|
352
429
|
videoStream = scrcpyClient.videoStream;
|
|
353
430
|
}
|
|
354
431
|
debugPage('video stream fetched successfully, metadata: %s', videoStream.metadata);
|
|
@@ -365,7 +442,7 @@ class ScrcpyServer {
|
|
|
365
442
|
}
|
|
366
443
|
debugPage('prepare to send video-metadata event to client, data: %s', JSON.stringify(metadata));
|
|
367
444
|
socket.emit('video-metadata', metadata);
|
|
368
|
-
debugPage('video-metadata event sent to client, id: %s', socket.id);
|
|
445
|
+
debugPage('video-metadata event sent to client, id: %s, timeout budget: %ss', socket.id, Math.round(SCRCPY_PREVIEW_METADATA_TIMEOUT_MS / 1000));
|
|
369
446
|
const { stream } = videoStream;
|
|
370
447
|
const reader = stream.getReader();
|
|
371
448
|
const processStream = async ()=>{
|
|
@@ -404,6 +481,14 @@ class ScrcpyServer {
|
|
|
404
481
|
if (scrcpyClient?.controller) socket.emit('control-ready');
|
|
405
482
|
} catch (error) {
|
|
406
483
|
console.error('failed to connect device:', error);
|
|
484
|
+
if (scrcpyClient) {
|
|
485
|
+
try {
|
|
486
|
+
await scrcpyClient.close();
|
|
487
|
+
} catch (closeError) {
|
|
488
|
+
console.error('failed to close scrcpy client after error:', closeError);
|
|
489
|
+
}
|
|
490
|
+
scrcpyClient = null;
|
|
491
|
+
}
|
|
407
492
|
socket.emit('error', {
|
|
408
493
|
message: `Failed to connect device: ${error?.message || 'Unknown error'}`
|
|
409
494
|
});
|
|
@@ -467,15 +552,15 @@ class ScrcpyServer {
|
|
|
467
552
|
if (this.httpServer) return this.httpServer.close();
|
|
468
553
|
}
|
|
469
554
|
constructor(){
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
555
|
+
scrcpy_server_define_property(this, "app", void 0);
|
|
556
|
+
scrcpy_server_define_property(this, "httpServer", void 0);
|
|
557
|
+
scrcpy_server_define_property(this, "io", void 0);
|
|
558
|
+
scrcpy_server_define_property(this, "port", void 0);
|
|
559
|
+
scrcpy_server_define_property(this, "defaultPort", SCRCPY_SERVER_PORT);
|
|
560
|
+
scrcpy_server_define_property(this, "adbClient", null);
|
|
561
|
+
scrcpy_server_define_property(this, "currentDeviceId", null);
|
|
562
|
+
scrcpy_server_define_property(this, "devicePollInterval", null);
|
|
563
|
+
scrcpy_server_define_property(this, "lastDeviceList", '');
|
|
479
564
|
this.app = express();
|
|
480
565
|
this.httpServer = createServer(this.app);
|
|
481
566
|
this.io = new Server(this.httpServer, {
|
package/dist/lib/bin.js
CHANGED
|
@@ -57,8 +57,8 @@ const androidPlaygroundPlatform = (0, playground_namespaceObject.definePlaygroun
|
|
|
57
57
|
async getSetupSchema () {
|
|
58
58
|
const targets = await getAdbTargets();
|
|
59
59
|
return {
|
|
60
|
-
title: '
|
|
61
|
-
description: 'Select an available ADB device to create the current Android Agent
|
|
60
|
+
title: 'Welcome to\nMidscene.js Playground!',
|
|
61
|
+
description: 'Select an available ADB device to create the current Android Agent',
|
|
62
62
|
primaryActionLabel: 'Create Agent',
|
|
63
63
|
autoSubmitWhenReady: 1 === targets.length,
|
|
64
64
|
fields: [
|
|
@@ -155,6 +155,24 @@ var external_cors_default = /*#__PURE__*/ __webpack_require__.n(external_cors_na
|
|
|
155
155
|
const external_express_namespaceObject = require("express");
|
|
156
156
|
var external_express_default = /*#__PURE__*/ __webpack_require__.n(external_express_namespaceObject);
|
|
157
157
|
const external_socket_io_namespaceObject = require("socket.io");
|
|
158
|
+
function getScrcpyPreviewStatusMessage(phase) {
|
|
159
|
+
switch(phase){
|
|
160
|
+
case 'connecting-device':
|
|
161
|
+
return 'Preparing Android device connection…';
|
|
162
|
+
case 'pushing-server':
|
|
163
|
+
return 'Uploading scrcpy runtime to device…';
|
|
164
|
+
case 'starting-service':
|
|
165
|
+
return 'Starting scrcpy service…';
|
|
166
|
+
case 'waiting-for-video':
|
|
167
|
+
return 'Waiting for first video frame…';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function buildScrcpyPreviewStatusEvent(phase) {
|
|
171
|
+
return {
|
|
172
|
+
phase,
|
|
173
|
+
message: getScrcpyPreviewStatusMessage(phase)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
158
176
|
function _define_property(obj, key, value) {
|
|
159
177
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
160
178
|
value: value,
|
|
@@ -165,6 +183,48 @@ function _define_property(obj, key, value) {
|
|
|
165
183
|
else obj[key] = value;
|
|
166
184
|
return obj;
|
|
167
185
|
}
|
|
186
|
+
class PromiseTimeoutError extends Error {
|
|
187
|
+
constructor(message, timeoutMs){
|
|
188
|
+
super(message), _define_property(this, "timeoutMs", void 0);
|
|
189
|
+
this.name = 'PromiseTimeoutError';
|
|
190
|
+
this.timeoutMs = timeoutMs;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function runTimeoutCallback(callback, context) {
|
|
194
|
+
if (!callback) return;
|
|
195
|
+
Promise.resolve().then(callback).catch((error)=>{
|
|
196
|
+
console.error(`Failed to run ${context}:`, error);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function withTimeout(promise, timeoutMs, message, options = {}) {
|
|
200
|
+
return new Promise((resolve, reject)=>{
|
|
201
|
+
let timedOut = false;
|
|
202
|
+
const timer = setTimeout(()=>{
|
|
203
|
+
timedOut = true;
|
|
204
|
+
runTimeoutCallback(options.onTimeout, 'timeout callback');
|
|
205
|
+
reject(new PromiseTimeoutError(message, timeoutMs));
|
|
206
|
+
}, timeoutMs);
|
|
207
|
+
Promise.resolve(promise).then((value)=>{
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
if (timedOut) return void runTimeoutCallback(()=>options.onSettledAfterTimeout?.(value), 'post-timeout settle callback');
|
|
210
|
+
resolve(value);
|
|
211
|
+
}, (error)=>{
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
if (timedOut) return void runTimeoutCallback(()=>options.onRejectedAfterTimeout?.(error), 'post-timeout rejection callback');
|
|
214
|
+
reject(error);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function scrcpy_server_define_property(obj, key, value) {
|
|
219
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
220
|
+
value: value,
|
|
221
|
+
enumerable: true,
|
|
222
|
+
configurable: true,
|
|
223
|
+
writable: true
|
|
224
|
+
});
|
|
225
|
+
else obj[key] = value;
|
|
226
|
+
return obj;
|
|
227
|
+
}
|
|
168
228
|
const debugPage = (0, logger_namespaceObject.getDebug)('android:playground');
|
|
169
229
|
const promiseExec = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.exec);
|
|
170
230
|
const LOOPBACK_HOSTS = new Set([
|
|
@@ -275,14 +335,15 @@ class ScrcpyServer {
|
|
|
275
335
|
return null;
|
|
276
336
|
}
|
|
277
337
|
}
|
|
278
|
-
async startScrcpy(adb, options = {}) {
|
|
338
|
+
async startScrcpy(adb, options = {}, onProgress) {
|
|
279
339
|
const { AdbScrcpyClient, AdbScrcpyOptions3_3_3 } = await import("@yume-chan/adb-scrcpy");
|
|
280
340
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
281
341
|
const { DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
282
342
|
const currentDir = __dirname;
|
|
283
343
|
const serverBinPath = external_node_path_default().resolve(currentDir, '../../bin/scrcpy-server');
|
|
284
344
|
try {
|
|
285
|
-
|
|
345
|
+
onProgress?.('pushing-server');
|
|
346
|
+
await withTimeout(AdbScrcpyClient.pushServer(adb, ReadableStream.from((0, external_node_fs_namespaceObject.createReadStream)(serverBinPath))), constants_namespaceObject.SCRCPY_PUSH_TIMEOUT_MS, `Timed out pushing scrcpy server to device after ${Math.round(constants_namespaceObject.SCRCPY_PUSH_TIMEOUT_MS / 1000)}s`);
|
|
286
347
|
const scrcpyOptions = new AdbScrcpyOptions3_3_3({
|
|
287
348
|
audio: false,
|
|
288
349
|
control: true,
|
|
@@ -291,7 +352,17 @@ class ScrcpyServer {
|
|
|
291
352
|
videoBitRate: 2000000,
|
|
292
353
|
...options
|
|
293
354
|
});
|
|
294
|
-
|
|
355
|
+
onProgress?.('starting-service');
|
|
356
|
+
const startPromise = AdbScrcpyClient.start(adb, DefaultServerPath, scrcpyOptions);
|
|
357
|
+
return await withTimeout(startPromise, constants_namespaceObject.SCRCPY_START_TIMEOUT_MS, `Timed out starting scrcpy service after ${Math.round(constants_namespaceObject.SCRCPY_START_TIMEOUT_MS / 1000)}s`, {
|
|
358
|
+
onSettledAfterTimeout: async (lateClient)=>{
|
|
359
|
+
try {
|
|
360
|
+
await lateClient.close();
|
|
361
|
+
} catch (closeError) {
|
|
362
|
+
console.error('failed to close late scrcpy client after timeout:', closeError);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
295
366
|
} catch (error) {
|
|
296
367
|
console.error('failed to start scrcpy:', error);
|
|
297
368
|
throw error;
|
|
@@ -302,6 +373,9 @@ class ScrcpyServer {
|
|
|
302
373
|
debugPage('client connected, id: %s, client address: %s', socket.id, socket.handshake.address);
|
|
303
374
|
let scrcpyClient = null;
|
|
304
375
|
let adb = null;
|
|
376
|
+
const emitPreviewStatus = (phase)=>{
|
|
377
|
+
socket.emit('preview-status', buildScrcpyPreviewStatusEvent(phase));
|
|
378
|
+
};
|
|
305
379
|
const sendDevicesList = async ()=>{
|
|
306
380
|
try {
|
|
307
381
|
debugPage('Socket request to get devices list');
|
|
@@ -350,6 +424,7 @@ class ScrcpyServer {
|
|
|
350
424
|
const { ScrcpyVideoCodecId } = await import("@yume-chan/scrcpy");
|
|
351
425
|
try {
|
|
352
426
|
debugPage('received device connection request, options: %s, client id: %s', options, socket.id);
|
|
427
|
+
emitPreviewStatus('connecting-device');
|
|
353
428
|
adb = await this.getAdb(this.currentDeviceId || void 0);
|
|
354
429
|
if (!adb) {
|
|
355
430
|
console.error('no available device found');
|
|
@@ -359,7 +434,7 @@ class ScrcpyServer {
|
|
|
359
434
|
return;
|
|
360
435
|
}
|
|
361
436
|
debugPage('starting scrcpy service, device id: %s', this.currentDeviceId);
|
|
362
|
-
scrcpyClient = await this.startScrcpy(adb, options);
|
|
437
|
+
scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
363
438
|
debugPage('scrcpy service started successfully');
|
|
364
439
|
debugPage('check scrcpyClient object structure: %s', Object.getOwnPropertyNames(scrcpyClient).map((name)=>{
|
|
365
440
|
const type = typeof scrcpyClient[name];
|
|
@@ -372,9 +447,11 @@ class ScrcpyServer {
|
|
|
372
447
|
let videoStream;
|
|
373
448
|
if ('object' == typeof scrcpyClient.videoStream && 'function' == typeof scrcpyClient.videoStream.then) {
|
|
374
449
|
debugPage('videoStream is a Promise, waiting for resolution...');
|
|
375
|
-
|
|
450
|
+
emitPreviewStatus('waiting-for-video');
|
|
451
|
+
videoStream = await withTimeout(scrcpyClient.videoStream, constants_namespaceObject.SCRCPY_VIDEO_STREAM_TIMEOUT_MS, `Timed out waiting for scrcpy video stream metadata after ${Math.round(constants_namespaceObject.SCRCPY_VIDEO_STREAM_TIMEOUT_MS / 1000)}s`);
|
|
376
452
|
} else {
|
|
377
453
|
debugPage('videoStream is not a Promise, directly use');
|
|
454
|
+
emitPreviewStatus('waiting-for-video');
|
|
378
455
|
videoStream = scrcpyClient.videoStream;
|
|
379
456
|
}
|
|
380
457
|
debugPage('video stream fetched successfully, metadata: %s', videoStream.metadata);
|
|
@@ -391,7 +468,7 @@ class ScrcpyServer {
|
|
|
391
468
|
}
|
|
392
469
|
debugPage('prepare to send video-metadata event to client, data: %s', JSON.stringify(metadata));
|
|
393
470
|
socket.emit('video-metadata', metadata);
|
|
394
|
-
debugPage('video-metadata event sent to client, id: %s', socket.id);
|
|
471
|
+
debugPage('video-metadata event sent to client, id: %s, timeout budget: %ss', socket.id, Math.round(constants_namespaceObject.SCRCPY_PREVIEW_METADATA_TIMEOUT_MS / 1000));
|
|
395
472
|
const { stream } = videoStream;
|
|
396
473
|
const reader = stream.getReader();
|
|
397
474
|
const processStream = async ()=>{
|
|
@@ -430,6 +507,14 @@ class ScrcpyServer {
|
|
|
430
507
|
if (scrcpyClient?.controller) socket.emit('control-ready');
|
|
431
508
|
} catch (error) {
|
|
432
509
|
console.error('failed to connect device:', error);
|
|
510
|
+
if (scrcpyClient) {
|
|
511
|
+
try {
|
|
512
|
+
await scrcpyClient.close();
|
|
513
|
+
} catch (closeError) {
|
|
514
|
+
console.error('failed to close scrcpy client after error:', closeError);
|
|
515
|
+
}
|
|
516
|
+
scrcpyClient = null;
|
|
517
|
+
}
|
|
433
518
|
socket.emit('error', {
|
|
434
519
|
message: `Failed to connect device: ${error?.message || 'Unknown error'}`
|
|
435
520
|
});
|
|
@@ -493,15 +578,15 @@ class ScrcpyServer {
|
|
|
493
578
|
if (this.httpServer) return this.httpServer.close();
|
|
494
579
|
}
|
|
495
580
|
constructor(){
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
581
|
+
scrcpy_server_define_property(this, "app", void 0);
|
|
582
|
+
scrcpy_server_define_property(this, "httpServer", void 0);
|
|
583
|
+
scrcpy_server_define_property(this, "io", void 0);
|
|
584
|
+
scrcpy_server_define_property(this, "port", void 0);
|
|
585
|
+
scrcpy_server_define_property(this, "defaultPort", constants_namespaceObject.SCRCPY_SERVER_PORT);
|
|
586
|
+
scrcpy_server_define_property(this, "adbClient", null);
|
|
587
|
+
scrcpy_server_define_property(this, "currentDeviceId", null);
|
|
588
|
+
scrcpy_server_define_property(this, "devicePollInterval", null);
|
|
589
|
+
scrcpy_server_define_property(this, "lastDeviceList", '');
|
|
505
590
|
this.app = external_express_default()();
|
|
506
591
|
this.httpServer = (0, external_node_http_namespaceObject.createServer)(this.app);
|
|
507
592
|
this.io = new external_socket_io_namespaceObject.Server(this.httpServer, {
|