@midscene/android-playground 1.10.4 → 1.10.5-beta-20260714101501.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/dist/es/bin.mjs +93 -50
- package/dist/es/index.mjs +93 -50
- package/dist/lib/bin.js +93 -50
- package/dist/lib/index.js +93 -50
- package/dist/types/scrcpy-preview-status.d.ts +8 -0
- package/dist/types/scrcpy-server.d.ts +2 -0
- package/package.json +5 -5
- package/static/index.html +1 -1
- package/static/static/css/index.2b6b43ad.css +2 -0
- package/static/static/css/index.2b6b43ad.css.map +1 -0
- package/static/static/js/{445.b67c87ae.js → 890.0322764c.js} +3 -3
- package/static/static/js/{445.b67c87ae.js.LICENSE.txt → 890.0322764c.js.LICENSE.txt} +0 -2
- package/static/static/js/890.0322764c.js.map +1 -0
- package/static/static/js/index.e8830865.js +960 -0
- package/static/static/js/index.e8830865.js.map +1 -0
- package/static/static/css/index.1293237f.css +0 -2
- package/static/static/css/index.1293237f.css.map +0 -1
- package/static/static/js/445.b67c87ae.js.map +0 -1
- package/static/static/js/index.4506e773.js +0 -960
- package/static/static/js/index.4506e773.js.map +0 -1
- /package/static/static/js/{index.4506e773.js.LICENSE.txt → index.e8830865.js.LICENSE.txt} +0 -0
package/dist/lib/bin.js
CHANGED
|
@@ -172,6 +172,14 @@ var external_cors_default = /*#__PURE__*/ __webpack_require__.n(external_cors_na
|
|
|
172
172
|
const external_express_namespaceObject = require("express");
|
|
173
173
|
var external_express_default = /*#__PURE__*/ __webpack_require__.n(external_express_namespaceObject);
|
|
174
174
|
const external_socket_io_namespaceObject = require("socket.io");
|
|
175
|
+
function buildScrcpyPreviewErrorEvent(reason, sessionId, message) {
|
|
176
|
+
return {
|
|
177
|
+
reason,
|
|
178
|
+
sessionId,
|
|
179
|
+
message,
|
|
180
|
+
recoverable: 'process-exited' === reason || 'startup-timeout' === reason || 'stream-ended' === reason || 'stream-read-failed' === reason
|
|
181
|
+
};
|
|
182
|
+
}
|
|
175
183
|
function getScrcpyPreviewStatusMessage(phase) {
|
|
176
184
|
switch(phase){
|
|
177
185
|
case 'connecting-device':
|
|
@@ -250,6 +258,11 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
250
258
|
'::1',
|
|
251
259
|
'[::1]'
|
|
252
260
|
]);
|
|
261
|
+
const MAX_SCRCPY_OUTPUT_LINES = 100;
|
|
262
|
+
function appendBoundedScrcpyOutput(outputLines, line, maxLines = MAX_SCRCPY_OUTPUT_LINES) {
|
|
263
|
+
outputLines.push(line);
|
|
264
|
+
if (outputLines.length > maxLines) outputLines.splice(0, outputLines.length - maxLines);
|
|
265
|
+
}
|
|
253
266
|
function isPrivateIP(hostname) {
|
|
254
267
|
return LOOPBACK_HOSTS.has(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) || /^192\.168\./.test(hostname) || /^100\.(6[4-9]|[7-9]\d|1[0-2]\d)\./.test(hostname);
|
|
255
268
|
}
|
|
@@ -411,8 +424,48 @@ class ScrcpyServer {
|
|
|
411
424
|
setupSocketHandlers() {
|
|
412
425
|
this.io.on('connection', async (socket)=>{
|
|
413
426
|
debugPage('client connected, id: %s, client address: %s', socket.id, socket.handshake.address);
|
|
414
|
-
let
|
|
427
|
+
let activeSession = null;
|
|
428
|
+
let sessionGeneration = 0;
|
|
415
429
|
let adb = null;
|
|
430
|
+
const closeScrcpySession = async (reason, sessionToClose = activeSession)=>{
|
|
431
|
+
if (!sessionToClose) return;
|
|
432
|
+
sessionToClose.closeReason ??= reason;
|
|
433
|
+
if (activeSession === sessionToClose) activeSession = null;
|
|
434
|
+
try {
|
|
435
|
+
debugPage('closing scrcpy session %s, reason: %s', sessionToClose.id, reason);
|
|
436
|
+
await sessionToClose.client.close();
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error(`failed to close scrcpy client (${reason}):`, error);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
const emitPreviewError = (session, reason, message)=>{
|
|
442
|
+
if (session.failureReported || session.closeReason) return;
|
|
443
|
+
session.failureReported = true;
|
|
444
|
+
debugPage('scrcpy session %s failed (%s), recent output:\n%s', session.id, reason, session.outputLines.join('\n'));
|
|
445
|
+
if (socket.connected) socket.emit('preview-error', buildScrcpyPreviewErrorEvent(reason, session.id, message));
|
|
446
|
+
};
|
|
447
|
+
const monitorScrcpySession = (session)=>{
|
|
448
|
+
(async ()=>{
|
|
449
|
+
try {
|
|
450
|
+
const reader = session.client.output.getReader();
|
|
451
|
+
while(true){
|
|
452
|
+
const { done, value } = await reader.read();
|
|
453
|
+
if (done) break;
|
|
454
|
+
for (const line of String(value).split(/\r?\n/))if (line) {
|
|
455
|
+
appendBoundedScrcpyOutput(session.outputLines, line);
|
|
456
|
+
debugPage('scrcpy[%s]: %s', session.id, line);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (!session.closeReason) debugPage('failed reading scrcpy output for %s: %s', session.id, error);
|
|
461
|
+
}
|
|
462
|
+
})();
|
|
463
|
+
Promise.resolve(session.client.exited).then(()=>{
|
|
464
|
+
emitPreviewError(session, 'process-exited', 'Scrcpy service exited. Recovering preview.');
|
|
465
|
+
}).catch((error)=>{
|
|
466
|
+
emitPreviewError(session, 'process-exited', `Scrcpy service exited unexpectedly: ${error instanceof Error ? error.message : String(error)}`);
|
|
467
|
+
});
|
|
468
|
+
};
|
|
416
469
|
const emitPreviewStatus = (phase)=>{
|
|
417
470
|
socket.emit('preview-status', buildScrcpyPreviewStatusEvent(phase));
|
|
418
471
|
};
|
|
@@ -439,10 +492,8 @@ class ScrcpyServer {
|
|
|
439
492
|
socket.on('switch-device', async (deviceId)=>{
|
|
440
493
|
debugPage('received client request to switch device:', deviceId);
|
|
441
494
|
try {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
scrcpyClient = null;
|
|
445
|
-
}
|
|
495
|
+
sessionGeneration += 1;
|
|
496
|
+
await closeScrcpySession('switch device');
|
|
446
497
|
this.currentDeviceId = deviceId;
|
|
447
498
|
debugPage('device switched to:', deviceId);
|
|
448
499
|
socket.emit('device-switched', {
|
|
@@ -461,21 +512,34 @@ class ScrcpyServer {
|
|
|
461
512
|
});
|
|
462
513
|
socket.on('connect-device', async (options = {})=>{
|
|
463
514
|
const { ScrcpyVideoCodecId } = await import("@yume-chan/scrcpy");
|
|
515
|
+
const generation = sessionGeneration + 1;
|
|
516
|
+
sessionGeneration = generation;
|
|
517
|
+
const sessionId = `${socket.id}:${generation}`;
|
|
518
|
+
const isCurrentGeneration = ()=>socket.connected && sessionGeneration === generation;
|
|
464
519
|
try {
|
|
465
520
|
debugPage('received device connection request, options: %s, client id: %s', options, socket.id);
|
|
521
|
+
await closeScrcpySession('new connect-device request');
|
|
466
522
|
emitPreviewStatus('connecting-device');
|
|
467
523
|
const requestedDeviceId = resolveRequestedDeviceId(options, this.currentDeviceId);
|
|
468
524
|
if (requestedDeviceId) this.currentDeviceId = requestedDeviceId;
|
|
469
525
|
adb = await this.getAdb(requestedDeviceId);
|
|
470
526
|
if (!adb) {
|
|
471
527
|
console.error('no available device found');
|
|
472
|
-
socket.emit('error',
|
|
473
|
-
message: 'No device found'
|
|
474
|
-
});
|
|
528
|
+
socket.emit('preview-error', buildScrcpyPreviewErrorEvent('adb-unavailable', sessionId, 'No Android device is available.'));
|
|
475
529
|
return;
|
|
476
530
|
}
|
|
531
|
+
if (!isCurrentGeneration()) return;
|
|
477
532
|
debugPage('starting scrcpy service, device id: %s', this.currentDeviceId);
|
|
478
|
-
scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
533
|
+
const scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
534
|
+
if (!isCurrentGeneration()) return void await scrcpyClient.close();
|
|
535
|
+
const session = {
|
|
536
|
+
client: scrcpyClient,
|
|
537
|
+
failureReported: false,
|
|
538
|
+
id: sessionId,
|
|
539
|
+
outputLines: []
|
|
540
|
+
};
|
|
541
|
+
activeSession = session;
|
|
542
|
+
monitorScrcpySession(session);
|
|
479
543
|
debugPage('scrcpy service started successfully');
|
|
480
544
|
debugPage('check scrcpyClient object structure: %s', Object.getOwnPropertyNames(scrcpyClient).map((name)=>{
|
|
481
545
|
const type = typeof scrcpyClient[name];
|
|
@@ -495,6 +559,7 @@ class ScrcpyServer {
|
|
|
495
559
|
emitPreviewStatus('waiting-for-video');
|
|
496
560
|
videoStream = scrcpyClient.videoStream;
|
|
497
561
|
}
|
|
562
|
+
if (!isCurrentGeneration() || activeSession !== session) return void await closeScrcpySession('stale video stream', session);
|
|
498
563
|
debugPage('video stream fetched successfully, metadata: %s', videoStream.metadata);
|
|
499
564
|
const metadata = videoStream.metadata || {};
|
|
500
565
|
debugPage('original metadata: %s', metadata);
|
|
@@ -511,14 +576,16 @@ class ScrcpyServer {
|
|
|
511
576
|
socket.emit('video-metadata', metadata);
|
|
512
577
|
debugPage('video-metadata event sent to client, id: %s, timeout budget: %ss', socket.id, Math.round(constants_namespaceObject.SCRCPY_PREVIEW_METADATA_TIMEOUT_MS / 1000));
|
|
513
578
|
const { stream } = videoStream;
|
|
579
|
+
const streamSession = session;
|
|
514
580
|
const reader = stream.getReader();
|
|
515
581
|
const processStream = async ()=>{
|
|
516
582
|
try {
|
|
517
583
|
while(true){
|
|
518
584
|
const { done, value } = await reader.read();
|
|
519
585
|
if (done) break;
|
|
586
|
+
if (!isCurrentGeneration() || activeSession !== streamSession) return;
|
|
520
587
|
const frameType = value.type || 'data';
|
|
521
|
-
socket.emit('video-data', {
|
|
588
|
+
socket.volatile.emit('video-data', {
|
|
522
589
|
data: value.data,
|
|
523
590
|
type: frameType,
|
|
524
591
|
timestamp: Date.now(),
|
|
@@ -527,63 +594,39 @@ class ScrcpyServer {
|
|
|
527
594
|
}
|
|
528
595
|
} catch (error) {
|
|
529
596
|
console.error('error processing video stream:', error);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
});
|
|
597
|
+
emitPreviewError(streamSession, 'stream-read-failed', `Scrcpy video stream failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
598
|
+
closeScrcpySession('video stream error', streamSession);
|
|
533
599
|
return;
|
|
534
600
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
});
|
|
538
|
-
if (scrcpyClient) {
|
|
539
|
-
try {
|
|
540
|
-
await scrcpyClient.close();
|
|
541
|
-
} catch (closeError) {
|
|
542
|
-
console.error('failed to close scrcpy client after stream ended:', closeError);
|
|
543
|
-
}
|
|
544
|
-
scrcpyClient = null;
|
|
545
|
-
}
|
|
601
|
+
emitPreviewError(streamSession, 'stream-ended', 'Scrcpy video stream ended. Recovering preview.');
|
|
602
|
+
closeScrcpySession('video stream ended', streamSession);
|
|
546
603
|
};
|
|
547
604
|
processStream();
|
|
548
605
|
} else {
|
|
549
606
|
console.error('scrcpyClient object does not have videoStream property');
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
607
|
+
emitPreviewError(session, 'unknown', 'Video stream is not available in the scrcpy client.');
|
|
608
|
+
await closeScrcpySession('video stream unavailable', session);
|
|
609
|
+
return;
|
|
553
610
|
}
|
|
554
611
|
} catch (error) {
|
|
555
612
|
console.error('error processing video stream:', error);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
});
|
|
613
|
+
emitPreviewError(session, error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', `Video stream processing error: ${error.message}`);
|
|
614
|
+
await closeScrcpySession('video stream setup error', session);
|
|
559
615
|
}
|
|
560
616
|
if (scrcpyClient?.controller) socket.emit('control-ready');
|
|
561
617
|
} catch (error) {
|
|
562
618
|
console.error('failed to connect device:', error);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
scrcpyClient = null;
|
|
570
|
-
}
|
|
571
|
-
socket.emit('error', {
|
|
572
|
-
message: `Failed to connect device: ${error?.message || 'Unknown error'}`
|
|
573
|
-
});
|
|
619
|
+
const session = activeSession;
|
|
620
|
+
if (session?.id === sessionId) {
|
|
621
|
+
emitPreviewError(session, error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', `Failed to connect device: ${error?.message || 'Unknown error'}`);
|
|
622
|
+
await closeScrcpySession('connect-device error', session);
|
|
623
|
+
} else if (isCurrentGeneration()) socket.emit('preview-error', buildScrcpyPreviewErrorEvent(error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', sessionId, `Failed to connect device: ${error?.message || 'Unknown error'}`));
|
|
574
624
|
}
|
|
575
625
|
});
|
|
576
626
|
socket.on('disconnect', async (reason)=>{
|
|
577
627
|
debugPage('client disconnected, id: %s, reason: %s', socket.id, reason);
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
debugPage('closing scrcpy client');
|
|
581
|
-
await scrcpyClient.close();
|
|
582
|
-
} catch (error) {
|
|
583
|
-
console.error('failed to close scrcpy client:', error);
|
|
584
|
-
}
|
|
585
|
-
scrcpyClient = null;
|
|
586
|
-
}
|
|
628
|
+
sessionGeneration += 1;
|
|
629
|
+
await closeScrcpySession(`socket disconnect: ${reason}`);
|
|
587
630
|
});
|
|
588
631
|
sendDevicesList();
|
|
589
632
|
});
|
package/dist/lib/index.js
CHANGED
|
@@ -187,6 +187,14 @@ var external_cors_default = /*#__PURE__*/ __webpack_require__.n(external_cors_na
|
|
|
187
187
|
const external_express_namespaceObject = require("express");
|
|
188
188
|
var external_express_default = /*#__PURE__*/ __webpack_require__.n(external_express_namespaceObject);
|
|
189
189
|
const external_socket_io_namespaceObject = require("socket.io");
|
|
190
|
+
function buildScrcpyPreviewErrorEvent(reason, sessionId, message) {
|
|
191
|
+
return {
|
|
192
|
+
reason,
|
|
193
|
+
sessionId,
|
|
194
|
+
message,
|
|
195
|
+
recoverable: 'process-exited' === reason || 'startup-timeout' === reason || 'stream-ended' === reason || 'stream-read-failed' === reason
|
|
196
|
+
};
|
|
197
|
+
}
|
|
190
198
|
function getScrcpyPreviewStatusMessage(phase) {
|
|
191
199
|
switch(phase){
|
|
192
200
|
case 'connecting-device':
|
|
@@ -265,6 +273,11 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
265
273
|
'::1',
|
|
266
274
|
'[::1]'
|
|
267
275
|
]);
|
|
276
|
+
const MAX_SCRCPY_OUTPUT_LINES = 100;
|
|
277
|
+
function appendBoundedScrcpyOutput(outputLines, line, maxLines = MAX_SCRCPY_OUTPUT_LINES) {
|
|
278
|
+
outputLines.push(line);
|
|
279
|
+
if (outputLines.length > maxLines) outputLines.splice(0, outputLines.length - maxLines);
|
|
280
|
+
}
|
|
268
281
|
function isPrivateIP(hostname) {
|
|
269
282
|
return LOOPBACK_HOSTS.has(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) || /^192\.168\./.test(hostname) || /^100\.(6[4-9]|[7-9]\d|1[0-2]\d)\./.test(hostname);
|
|
270
283
|
}
|
|
@@ -426,8 +439,48 @@ class ScrcpyServer {
|
|
|
426
439
|
setupSocketHandlers() {
|
|
427
440
|
this.io.on('connection', async (socket)=>{
|
|
428
441
|
debugPage('client connected, id: %s, client address: %s', socket.id, socket.handshake.address);
|
|
429
|
-
let
|
|
442
|
+
let activeSession = null;
|
|
443
|
+
let sessionGeneration = 0;
|
|
430
444
|
let adb = null;
|
|
445
|
+
const closeScrcpySession = async (reason, sessionToClose = activeSession)=>{
|
|
446
|
+
if (!sessionToClose) return;
|
|
447
|
+
sessionToClose.closeReason ??= reason;
|
|
448
|
+
if (activeSession === sessionToClose) activeSession = null;
|
|
449
|
+
try {
|
|
450
|
+
debugPage('closing scrcpy session %s, reason: %s', sessionToClose.id, reason);
|
|
451
|
+
await sessionToClose.client.close();
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.error(`failed to close scrcpy client (${reason}):`, error);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
const emitPreviewError = (session, reason, message)=>{
|
|
457
|
+
if (session.failureReported || session.closeReason) return;
|
|
458
|
+
session.failureReported = true;
|
|
459
|
+
debugPage('scrcpy session %s failed (%s), recent output:\n%s', session.id, reason, session.outputLines.join('\n'));
|
|
460
|
+
if (socket.connected) socket.emit('preview-error', buildScrcpyPreviewErrorEvent(reason, session.id, message));
|
|
461
|
+
};
|
|
462
|
+
const monitorScrcpySession = (session)=>{
|
|
463
|
+
(async ()=>{
|
|
464
|
+
try {
|
|
465
|
+
const reader = session.client.output.getReader();
|
|
466
|
+
while(true){
|
|
467
|
+
const { done, value } = await reader.read();
|
|
468
|
+
if (done) break;
|
|
469
|
+
for (const line of String(value).split(/\r?\n/))if (line) {
|
|
470
|
+
appendBoundedScrcpyOutput(session.outputLines, line);
|
|
471
|
+
debugPage('scrcpy[%s]: %s', session.id, line);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (!session.closeReason) debugPage('failed reading scrcpy output for %s: %s', session.id, error);
|
|
476
|
+
}
|
|
477
|
+
})();
|
|
478
|
+
Promise.resolve(session.client.exited).then(()=>{
|
|
479
|
+
emitPreviewError(session, 'process-exited', 'Scrcpy service exited. Recovering preview.');
|
|
480
|
+
}).catch((error)=>{
|
|
481
|
+
emitPreviewError(session, 'process-exited', `Scrcpy service exited unexpectedly: ${error instanceof Error ? error.message : String(error)}`);
|
|
482
|
+
});
|
|
483
|
+
};
|
|
431
484
|
const emitPreviewStatus = (phase)=>{
|
|
432
485
|
socket.emit('preview-status', buildScrcpyPreviewStatusEvent(phase));
|
|
433
486
|
};
|
|
@@ -454,10 +507,8 @@ class ScrcpyServer {
|
|
|
454
507
|
socket.on('switch-device', async (deviceId)=>{
|
|
455
508
|
debugPage('received client request to switch device:', deviceId);
|
|
456
509
|
try {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
scrcpyClient = null;
|
|
460
|
-
}
|
|
510
|
+
sessionGeneration += 1;
|
|
511
|
+
await closeScrcpySession('switch device');
|
|
461
512
|
this.currentDeviceId = deviceId;
|
|
462
513
|
debugPage('device switched to:', deviceId);
|
|
463
514
|
socket.emit('device-switched', {
|
|
@@ -476,21 +527,34 @@ class ScrcpyServer {
|
|
|
476
527
|
});
|
|
477
528
|
socket.on('connect-device', async (options = {})=>{
|
|
478
529
|
const { ScrcpyVideoCodecId } = await import("@yume-chan/scrcpy");
|
|
530
|
+
const generation = sessionGeneration + 1;
|
|
531
|
+
sessionGeneration = generation;
|
|
532
|
+
const sessionId = `${socket.id}:${generation}`;
|
|
533
|
+
const isCurrentGeneration = ()=>socket.connected && sessionGeneration === generation;
|
|
479
534
|
try {
|
|
480
535
|
debugPage('received device connection request, options: %s, client id: %s', options, socket.id);
|
|
536
|
+
await closeScrcpySession('new connect-device request');
|
|
481
537
|
emitPreviewStatus('connecting-device');
|
|
482
538
|
const requestedDeviceId = resolveRequestedDeviceId(options, this.currentDeviceId);
|
|
483
539
|
if (requestedDeviceId) this.currentDeviceId = requestedDeviceId;
|
|
484
540
|
adb = await this.getAdb(requestedDeviceId);
|
|
485
541
|
if (!adb) {
|
|
486
542
|
console.error('no available device found');
|
|
487
|
-
socket.emit('error',
|
|
488
|
-
message: 'No device found'
|
|
489
|
-
});
|
|
543
|
+
socket.emit('preview-error', buildScrcpyPreviewErrorEvent('adb-unavailable', sessionId, 'No Android device is available.'));
|
|
490
544
|
return;
|
|
491
545
|
}
|
|
546
|
+
if (!isCurrentGeneration()) return;
|
|
492
547
|
debugPage('starting scrcpy service, device id: %s', this.currentDeviceId);
|
|
493
|
-
scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
548
|
+
const scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
549
|
+
if (!isCurrentGeneration()) return void await scrcpyClient.close();
|
|
550
|
+
const session = {
|
|
551
|
+
client: scrcpyClient,
|
|
552
|
+
failureReported: false,
|
|
553
|
+
id: sessionId,
|
|
554
|
+
outputLines: []
|
|
555
|
+
};
|
|
556
|
+
activeSession = session;
|
|
557
|
+
monitorScrcpySession(session);
|
|
494
558
|
debugPage('scrcpy service started successfully');
|
|
495
559
|
debugPage('check scrcpyClient object structure: %s', Object.getOwnPropertyNames(scrcpyClient).map((name)=>{
|
|
496
560
|
const type = typeof scrcpyClient[name];
|
|
@@ -510,6 +574,7 @@ class ScrcpyServer {
|
|
|
510
574
|
emitPreviewStatus('waiting-for-video');
|
|
511
575
|
videoStream = scrcpyClient.videoStream;
|
|
512
576
|
}
|
|
577
|
+
if (!isCurrentGeneration() || activeSession !== session) return void await closeScrcpySession('stale video stream', session);
|
|
513
578
|
debugPage('video stream fetched successfully, metadata: %s', videoStream.metadata);
|
|
514
579
|
const metadata = videoStream.metadata || {};
|
|
515
580
|
debugPage('original metadata: %s', metadata);
|
|
@@ -526,14 +591,16 @@ class ScrcpyServer {
|
|
|
526
591
|
socket.emit('video-metadata', metadata);
|
|
527
592
|
debugPage('video-metadata event sent to client, id: %s, timeout budget: %ss', socket.id, Math.round(constants_namespaceObject.SCRCPY_PREVIEW_METADATA_TIMEOUT_MS / 1000));
|
|
528
593
|
const { stream } = videoStream;
|
|
594
|
+
const streamSession = session;
|
|
529
595
|
const reader = stream.getReader();
|
|
530
596
|
const processStream = async ()=>{
|
|
531
597
|
try {
|
|
532
598
|
while(true){
|
|
533
599
|
const { done, value } = await reader.read();
|
|
534
600
|
if (done) break;
|
|
601
|
+
if (!isCurrentGeneration() || activeSession !== streamSession) return;
|
|
535
602
|
const frameType = value.type || 'data';
|
|
536
|
-
socket.emit('video-data', {
|
|
603
|
+
socket.volatile.emit('video-data', {
|
|
537
604
|
data: value.data,
|
|
538
605
|
type: frameType,
|
|
539
606
|
timestamp: Date.now(),
|
|
@@ -542,63 +609,39 @@ class ScrcpyServer {
|
|
|
542
609
|
}
|
|
543
610
|
} catch (error) {
|
|
544
611
|
console.error('error processing video stream:', error);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
});
|
|
612
|
+
emitPreviewError(streamSession, 'stream-read-failed', `Scrcpy video stream failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
613
|
+
closeScrcpySession('video stream error', streamSession);
|
|
548
614
|
return;
|
|
549
615
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
});
|
|
553
|
-
if (scrcpyClient) {
|
|
554
|
-
try {
|
|
555
|
-
await scrcpyClient.close();
|
|
556
|
-
} catch (closeError) {
|
|
557
|
-
console.error('failed to close scrcpy client after stream ended:', closeError);
|
|
558
|
-
}
|
|
559
|
-
scrcpyClient = null;
|
|
560
|
-
}
|
|
616
|
+
emitPreviewError(streamSession, 'stream-ended', 'Scrcpy video stream ended. Recovering preview.');
|
|
617
|
+
closeScrcpySession('video stream ended', streamSession);
|
|
561
618
|
};
|
|
562
619
|
processStream();
|
|
563
620
|
} else {
|
|
564
621
|
console.error('scrcpyClient object does not have videoStream property');
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
622
|
+
emitPreviewError(session, 'unknown', 'Video stream is not available in the scrcpy client.');
|
|
623
|
+
await closeScrcpySession('video stream unavailable', session);
|
|
624
|
+
return;
|
|
568
625
|
}
|
|
569
626
|
} catch (error) {
|
|
570
627
|
console.error('error processing video stream:', error);
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
});
|
|
628
|
+
emitPreviewError(session, error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', `Video stream processing error: ${error.message}`);
|
|
629
|
+
await closeScrcpySession('video stream setup error', session);
|
|
574
630
|
}
|
|
575
631
|
if (scrcpyClient?.controller) socket.emit('control-ready');
|
|
576
632
|
} catch (error) {
|
|
577
633
|
console.error('failed to connect device:', error);
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
scrcpyClient = null;
|
|
585
|
-
}
|
|
586
|
-
socket.emit('error', {
|
|
587
|
-
message: `Failed to connect device: ${error?.message || 'Unknown error'}`
|
|
588
|
-
});
|
|
634
|
+
const session = activeSession;
|
|
635
|
+
if (session?.id === sessionId) {
|
|
636
|
+
emitPreviewError(session, error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', `Failed to connect device: ${error?.message || 'Unknown error'}`);
|
|
637
|
+
await closeScrcpySession('connect-device error', session);
|
|
638
|
+
} else if (isCurrentGeneration()) socket.emit('preview-error', buildScrcpyPreviewErrorEvent(error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', sessionId, `Failed to connect device: ${error?.message || 'Unknown error'}`));
|
|
589
639
|
}
|
|
590
640
|
});
|
|
591
641
|
socket.on('disconnect', async (reason)=>{
|
|
592
642
|
debugPage('client disconnected, id: %s, reason: %s', socket.id, reason);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
debugPage('closing scrcpy client');
|
|
596
|
-
await scrcpyClient.close();
|
|
597
|
-
} catch (error) {
|
|
598
|
-
console.error('failed to close scrcpy client:', error);
|
|
599
|
-
}
|
|
600
|
-
scrcpyClient = null;
|
|
601
|
-
}
|
|
643
|
+
sessionGeneration += 1;
|
|
644
|
+
await closeScrcpySession(`socket disconnect: ${reason}`);
|
|
602
645
|
});
|
|
603
646
|
sendDevicesList();
|
|
604
647
|
});
|
|
@@ -3,5 +3,13 @@ export interface ScrcpyPreviewStatusEvent {
|
|
|
3
3
|
phase: ScrcpyPreviewPhase;
|
|
4
4
|
message: string;
|
|
5
5
|
}
|
|
6
|
+
export type ScrcpyPreviewErrorReason = 'adb-unavailable' | 'process-exited' | 'startup-timeout' | 'stream-ended' | 'stream-read-failed' | 'unknown';
|
|
7
|
+
export interface ScrcpyPreviewErrorEvent {
|
|
8
|
+
message: string;
|
|
9
|
+
reason: ScrcpyPreviewErrorReason;
|
|
10
|
+
recoverable: boolean;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildScrcpyPreviewErrorEvent(reason: ScrcpyPreviewErrorReason, sessionId: string, message: string): ScrcpyPreviewErrorEvent;
|
|
6
14
|
export declare function getScrcpyPreviewStatusMessage(phase: ScrcpyPreviewPhase): string;
|
|
7
15
|
export declare function buildScrcpyPreviewStatusEvent(phase: ScrcpyPreviewPhase): ScrcpyPreviewStatusEvent;
|
|
@@ -3,9 +3,11 @@ import type { AdbServerClient } from '@yume-chan/adb';
|
|
|
3
3
|
import express from 'express';
|
|
4
4
|
import { Server } from 'socket.io';
|
|
5
5
|
export declare const debugPage: import("@midscene/shared/logger").DebugFunction;
|
|
6
|
+
export declare function appendBoundedScrcpyOutput(outputLines: string[], line: string, maxLines?: number): void;
|
|
6
7
|
export interface ScrcpyConnectDeviceRequest {
|
|
7
8
|
deviceId?: string;
|
|
8
9
|
maxSize?: number;
|
|
10
|
+
videoBitRate?: number;
|
|
9
11
|
}
|
|
10
12
|
export interface ScrcpyListedDevice {
|
|
11
13
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@midscene/android-playground",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.5-beta-20260714101501.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/web-infra-dev/midscene.git",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"express": "^4.21.2",
|
|
40
40
|
"open": "10.1.0",
|
|
41
41
|
"socket.io": "^4.8.1",
|
|
42
|
-
"@midscene/android": "1.10.
|
|
43
|
-
"@midscene/
|
|
44
|
-
"@midscene/
|
|
45
|
-
"@midscene/
|
|
42
|
+
"@midscene/android": "1.10.5-beta-20260714101501.0",
|
|
43
|
+
"@midscene/core": "1.10.5-beta-20260714101501.0",
|
|
44
|
+
"@midscene/shared": "1.10.5-beta-20260714101501.0",
|
|
45
|
+
"@midscene/playground": "1.10.5-beta-20260714101501.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@rslib/core": "^0.18.3",
|
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.ed140d90.js"></script><script defer src="/static/js/
|
|
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.ed140d90.js"></script><script defer src="/static/js/890.0322764c.js"></script><script defer src="/static/js/index.e8830865.js"></script><link href="/static/css/index.2b6b43ad.css" rel="stylesheet"></head><body><div id="root"></div></body></html>
|