@aiscene/aiserver 2.0.4 → 2.0.5
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/api/device-api.d.ts.map +1 -1
- package/dist/api/device-api.js +0 -2
- package/dist/api/device-api.js.map +1 -1
- package/dist/config/cli.d.ts +24 -0
- package/dist/config/cli.d.ts.map +1 -1
- package/dist/config/cli.js +96 -0
- package/dist/config/cli.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +20 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +12 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/core/types.d.ts +0 -6
- package/dist/core/types.d.ts.map +1 -1
- package/dist/debug/websocket-server.d.ts.map +1 -1
- package/dist/debug/websocket-server.js +23 -35
- package/dist/debug/websocket-server.js.map +1 -1
- package/dist/device/detector.d.ts +0 -2
- package/dist/device/detector.d.ts.map +1 -1
- package/dist/device/detector.js +3 -70
- package/dist/device/detector.js.map +1 -1
- package/dist/device/heartbeat.d.ts.map +1 -1
- package/dist/device/heartbeat.js +2 -14
- package/dist/device/heartbeat.js.map +1 -1
- package/dist/device/types.d.ts +0 -3
- package/dist/device/types.d.ts.map +1 -1
- package/dist/executor/ios-executor.d.ts.map +1 -1
- package/dist/executor/ios-executor.js +15 -29
- package/dist/executor/ios-executor.js.map +1 -1
- package/dist/goose-runtime/git.d.ts +4 -0
- package/dist/goose-runtime/git.d.ts.map +1 -0
- package/dist/goose-runtime/git.js +63 -0
- package/dist/goose-runtime/git.js.map +1 -0
- package/dist/goose-runtime/runtime.d.ts +5 -0
- package/dist/goose-runtime/runtime.d.ts.map +1 -0
- package/dist/goose-runtime/runtime.js +141 -0
- package/dist/goose-runtime/runtime.js.map +1 -0
- package/dist/goose-runtime/types.d.ts +23 -0
- package/dist/goose-runtime/types.d.ts.map +1 -0
- package/dist/goose-runtime/types.js +2 -0
- package/dist/goose-runtime/types.js.map +1 -0
- package/dist/goose-runtime/workspace.d.ts +23 -0
- package/dist/goose-runtime/workspace.d.ts.map +1 -0
- package/dist/goose-runtime/workspace.js +200 -0
- package/dist/goose-runtime/workspace.js.map +1 -0
- package/dist/scrcpy/preview-status.js +2 -2
- package/dist/scrcpy/preview-status.js.map +1 -1
- package/dist/scrcpy/server.d.ts +0 -14
- package/dist/scrcpy/server.d.ts.map +1 -1
- package/dist/scrcpy/server.js +15 -472
- package/dist/scrcpy/server.js.map +1 -1
- package/dist/storage/repositories/device-repo.d.ts.map +1 -1
- package/dist/storage/repositories/device-repo.js +0 -1
- package/dist/storage/repositories/device-repo.js.map +1 -1
- package/dist/task/guada-online-batch-poller.d.ts +0 -10
- package/dist/task/guada-online-batch-poller.d.ts.map +1 -1
- package/dist/task/guada-online-batch-poller.js +8 -137
- package/dist/task/guada-online-batch-poller.js.map +1 -1
- package/dist/task/scheduler.d.ts +5 -0
- package/dist/task/scheduler.d.ts.map +1 -1
- package/dist/task/scheduler.js +185 -95
- package/dist/task/scheduler.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +10 -23
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/dist/scrcpy/server.js
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* (视频流走二进制帧,不走 JSON)
|
|
49
49
|
*/
|
|
50
50
|
import { createReadStream, existsSync } from 'node:fs';
|
|
51
|
-
import { createServer
|
|
51
|
+
import { createServer } from 'node:http';
|
|
52
52
|
import path from 'node:path';
|
|
53
53
|
import { createRequire } from 'node:module';
|
|
54
54
|
import { randomUUID } from 'node:crypto';
|
|
@@ -61,8 +61,6 @@ import { withTimeout } from './timeout.js';
|
|
|
61
61
|
import { androidRecordingManager } from '../recorder/android-recording-manager.js';
|
|
62
62
|
import { replayAndroidLoginScript } from '../recorder/android-login-replay.js';
|
|
63
63
|
import { captureAndroidElementState, captureAndroidScreenshotState, getAndroidScreenSize, runAdb, } from '../recorder/android-ui-hierarchy.js';
|
|
64
|
-
import { deviceDetector } from '../device/detector.js';
|
|
65
|
-
import { ensureIOSWda } from '../executor/ios-wda-manager.js';
|
|
66
64
|
const logger = createLogger('ScrcpyServer');
|
|
67
65
|
const requireCjs = createRequire(import.meta.url);
|
|
68
66
|
function resolveRequestedDeviceId(options, currentDeviceId) {
|
|
@@ -227,7 +225,6 @@ export class ScrcpyServer {
|
|
|
227
225
|
id: d.serial,
|
|
228
226
|
name: d.product || d.model || d.serial,
|
|
229
227
|
status: d.state || 'device',
|
|
230
|
-
platform: 'android',
|
|
231
228
|
}));
|
|
232
229
|
}
|
|
233
230
|
catch (error) {
|
|
@@ -235,22 +232,6 @@ export class ScrcpyServer {
|
|
|
235
232
|
return [];
|
|
236
233
|
}
|
|
237
234
|
}
|
|
238
|
-
async loadDevicesList() {
|
|
239
|
-
const [androidDevices, iosDevices] = await Promise.all([
|
|
240
|
-
this.loadDevicesListFromAdb(),
|
|
241
|
-
deviceDetector.detectIOSDevices().catch((error) => {
|
|
242
|
-
logger.debug(`Failed to detect iOS devices for projection: ${error.message}`);
|
|
243
|
-
return [];
|
|
244
|
-
}),
|
|
245
|
-
]);
|
|
246
|
-
const listedIOSDevices = iosDevices.map((device) => ({
|
|
247
|
-
id: device.udid,
|
|
248
|
-
name: device.name || device.model || device.udid,
|
|
249
|
-
status: device.status || 'device',
|
|
250
|
-
platform: 'ios',
|
|
251
|
-
}));
|
|
252
|
-
return [...androidDevices, ...listedIOSDevices];
|
|
253
|
-
}
|
|
254
235
|
async getDevicesList(forceRefresh = false) {
|
|
255
236
|
const now = Date.now();
|
|
256
237
|
const cacheTtlMs = 2000;
|
|
@@ -262,7 +243,7 @@ export class ScrcpyServer {
|
|
|
262
243
|
if (this.deviceListInFlight) {
|
|
263
244
|
return this.deviceListInFlight;
|
|
264
245
|
}
|
|
265
|
-
this.deviceListInFlight = this.
|
|
246
|
+
this.deviceListInFlight = this.loadDevicesListFromAdb()
|
|
266
247
|
.then((devices) => {
|
|
267
248
|
this.lastDeviceList = devices;
|
|
268
249
|
this.lastDeviceListAt = Date.now();
|
|
@@ -462,8 +443,6 @@ export class ScrcpyServer {
|
|
|
462
443
|
videoWidth: 0,
|
|
463
444
|
videoHeight: 0,
|
|
464
445
|
streamReader: null,
|
|
465
|
-
platform: null,
|
|
466
|
-
iosProjection: null,
|
|
467
446
|
};
|
|
468
447
|
this.sessions.set(ws, state);
|
|
469
448
|
this.attachSocketHandlers(state);
|
|
@@ -511,324 +490,8 @@ export class ScrcpyServer {
|
|
|
511
490
|
}
|
|
512
491
|
state.scrcpyClient = null;
|
|
513
492
|
}
|
|
514
|
-
if (state.iosProjection) {
|
|
515
|
-
const projection = state.iosProjection;
|
|
516
|
-
projection.stopped = true;
|
|
517
|
-
try {
|
|
518
|
-
projection.cleanup?.();
|
|
519
|
-
}
|
|
520
|
-
catch {
|
|
521
|
-
// ignore
|
|
522
|
-
}
|
|
523
|
-
await this.deleteIOSWdaSession(projection);
|
|
524
|
-
state.iosProjection = null;
|
|
525
|
-
}
|
|
526
493
|
state.videoWidth = 0;
|
|
527
494
|
state.videoHeight = 0;
|
|
528
|
-
state.platform = null;
|
|
529
|
-
}
|
|
530
|
-
async connectIOSDeviceForSession(state, deviceId, emitStatus) {
|
|
531
|
-
const { ws } = state;
|
|
532
|
-
emitStatus('starting-service');
|
|
533
|
-
const wda = await ensureIOSWda({
|
|
534
|
-
udid: deviceId,
|
|
535
|
-
useXcodebuild: false,
|
|
536
|
-
log: (message) => logger.info(`[iOSProjection] ${message}`),
|
|
537
|
-
});
|
|
538
|
-
if (!this.isSessionOpen(state))
|
|
539
|
-
return;
|
|
540
|
-
const wdaSession = await this.createIOSWdaSession(wda.host, wda.port);
|
|
541
|
-
const projection = {
|
|
542
|
-
stopped: false,
|
|
543
|
-
wdaHost: wda.host,
|
|
544
|
-
wdaPort: wda.port,
|
|
545
|
-
sessionId: wdaSession.sessionId,
|
|
546
|
-
controlWidth: wdaSession.width,
|
|
547
|
-
controlHeight: wdaSession.height,
|
|
548
|
-
};
|
|
549
|
-
state.iosProjection = projection;
|
|
550
|
-
state.platform = 'ios';
|
|
551
|
-
state.deviceId = deviceId;
|
|
552
|
-
this.currentDeviceId = deviceId;
|
|
553
|
-
emitStatus('waiting-for-video');
|
|
554
|
-
logger.info(`iOS projection started for device=${deviceId},wda=http://${wda.host}:${wda.port},mjpeg=http://${wda.host}:${wda.mjpegPort},client=${state.id}`);
|
|
555
|
-
const initialFrame = await this.captureIOSWdaScreenshot(wda.host, wda.port);
|
|
556
|
-
if (!this.isSessionOpen(state) || state.iosProjection !== projection || projection.stopped)
|
|
557
|
-
return;
|
|
558
|
-
state.videoWidth = initialFrame.width;
|
|
559
|
-
state.videoHeight = initialFrame.height;
|
|
560
|
-
wsSend(ws, 'video-metadata', {
|
|
561
|
-
codec: 'mjpeg',
|
|
562
|
-
width: initialFrame.width,
|
|
563
|
-
height: initialFrame.height,
|
|
564
|
-
platform: 'ios',
|
|
565
|
-
});
|
|
566
|
-
this.sendIOSImageFrame(ws, {
|
|
567
|
-
...initialFrame,
|
|
568
|
-
mime: 'image/png',
|
|
569
|
-
timestamp: Date.now(),
|
|
570
|
-
source: 'screenshot',
|
|
571
|
-
});
|
|
572
|
-
const startScreenshotFallback = () => {
|
|
573
|
-
void this.startIOSWdaScreenshotLoop(state, projection, wda.host, wda.port);
|
|
574
|
-
};
|
|
575
|
-
const nativeStarted = await this.startIOSNativeMjpegStream(state, projection, wda.host, wda.mjpegPort, startScreenshotFallback);
|
|
576
|
-
if (!nativeStarted) {
|
|
577
|
-
logger.info(`iOS native MJPEG unavailable, falling back to WDA screenshots: device=${deviceId}`);
|
|
578
|
-
startScreenshotFallback();
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
sendIOSImageFrame(ws, frame) {
|
|
582
|
-
wsSend(ws, 'ios-screenshot-frame', frame);
|
|
583
|
-
}
|
|
584
|
-
async createIOSWdaSession(host, port) {
|
|
585
|
-
let sessionId = null;
|
|
586
|
-
try {
|
|
587
|
-
const response = await this.iosWdaRequest(host, port, 'POST', '/session', {
|
|
588
|
-
capabilities: {
|
|
589
|
-
alwaysMatch: {
|
|
590
|
-
platformName: 'iOS',
|
|
591
|
-
automationName: 'XCUITest',
|
|
592
|
-
shouldUseSingletonTestManager: false,
|
|
593
|
-
shouldUseTestManagerForVisibilityDetection: false,
|
|
594
|
-
},
|
|
595
|
-
},
|
|
596
|
-
});
|
|
597
|
-
sessionId = this.extractIOSWdaSessionId(response);
|
|
598
|
-
if (sessionId) {
|
|
599
|
-
logger.info(`iOS WDA session created for projection: ${sessionId}`);
|
|
600
|
-
await this.configureIOSWdaSession(host, port, sessionId);
|
|
601
|
-
}
|
|
602
|
-
else {
|
|
603
|
-
logger.warn('iOS WDA session response did not include sessionId; touch control will be disabled');
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
catch (error) {
|
|
607
|
-
logger.warn(`iOS WDA session creation failed, projection will be view-only: ${error.message}`);
|
|
608
|
-
}
|
|
609
|
-
const size = sessionId
|
|
610
|
-
? await this.getIOSWdaWindowSize(host, port, sessionId).catch((error) => {
|
|
611
|
-
logger.warn(`iOS WDA window size failed, using screenshot size for touch mapping: ${error.message}`);
|
|
612
|
-
return { width: 0, height: 0 };
|
|
613
|
-
})
|
|
614
|
-
: { width: 0, height: 0 };
|
|
615
|
-
return { sessionId, width: size.width, height: size.height };
|
|
616
|
-
}
|
|
617
|
-
async configureIOSWdaSession(host, port, sessionId) {
|
|
618
|
-
try {
|
|
619
|
-
await this.iosWdaRequest(host, port, 'POST', `/session/${sessionId}/appium/settings`, {
|
|
620
|
-
snapshotMaxDepth: 50,
|
|
621
|
-
elementResponseAttributes: 'type,label,name,value,rect,enabled,visible',
|
|
622
|
-
mjpegServerScreenshotQuality: 50,
|
|
623
|
-
mjpegServerFramerate: 30,
|
|
624
|
-
mjpegScalingFactor: 50,
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
catch (error) {
|
|
628
|
-
logger.debug(`iOS WDA settings update skipped: ${error.message}`);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
async getIOSWdaWindowSize(host, port, sessionId) {
|
|
632
|
-
const response = await this.iosWdaRequest(host, port, 'GET', `/session/${sessionId}/window/size`);
|
|
633
|
-
const value = response?.value || response;
|
|
634
|
-
const width = Number(value?.width);
|
|
635
|
-
const height = Number(value?.height);
|
|
636
|
-
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
637
|
-
throw new Error(`invalid WDA window size: ${JSON.stringify(response)}`);
|
|
638
|
-
}
|
|
639
|
-
return { width, height };
|
|
640
|
-
}
|
|
641
|
-
extractIOSWdaSessionId(response) {
|
|
642
|
-
const candidates = [
|
|
643
|
-
response?.sessionId,
|
|
644
|
-
response?.value?.sessionId,
|
|
645
|
-
response?.value?.session_id,
|
|
646
|
-
];
|
|
647
|
-
const matched = candidates.find((item) => typeof item === 'string' && item.trim());
|
|
648
|
-
return matched ? matched.trim() : null;
|
|
649
|
-
}
|
|
650
|
-
async iosWdaRequest(host, port, method, endpoint, body) {
|
|
651
|
-
const controller = new AbortController();
|
|
652
|
-
const timer = setTimeout(() => controller.abort(), 8000);
|
|
653
|
-
try {
|
|
654
|
-
const response = await fetch(`http://${host}:${port}${endpoint}`, {
|
|
655
|
-
method,
|
|
656
|
-
signal: controller.signal,
|
|
657
|
-
headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
|
|
658
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
659
|
-
});
|
|
660
|
-
const text = await response.text();
|
|
661
|
-
const data = text ? JSON.parse(text) : {};
|
|
662
|
-
if (!response.ok) {
|
|
663
|
-
throw new Error(`WDA ${method} ${endpoint} HTTP ${response.status}: ${text}`);
|
|
664
|
-
}
|
|
665
|
-
return data;
|
|
666
|
-
}
|
|
667
|
-
finally {
|
|
668
|
-
clearTimeout(timer);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
async deleteIOSWdaSession(projection) {
|
|
672
|
-
if (!projection.sessionId)
|
|
673
|
-
return;
|
|
674
|
-
try {
|
|
675
|
-
await this.iosWdaRequest(projection.wdaHost, projection.wdaPort, 'DELETE', `/session/${projection.sessionId}`);
|
|
676
|
-
}
|
|
677
|
-
catch (error) {
|
|
678
|
-
logger.debug(`delete iOS WDA session skipped: ${error.message}`);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
async startIOSWdaScreenshotLoop(state, projection, host, port) {
|
|
682
|
-
const { ws } = state;
|
|
683
|
-
try {
|
|
684
|
-
while (this.isSessionOpen(state) && state.iosProjection === projection && !projection.stopped) {
|
|
685
|
-
try {
|
|
686
|
-
const frame = await this.captureIOSWdaScreenshot(host, port);
|
|
687
|
-
this.sendIOSImageFrame(ws, {
|
|
688
|
-
...frame,
|
|
689
|
-
mime: 'image/png',
|
|
690
|
-
timestamp: Date.now(),
|
|
691
|
-
source: 'screenshot',
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
catch (error) {
|
|
695
|
-
logger.warn(`iOS screenshot frame failed: ${error.message}`);
|
|
696
|
-
wsSend(ws, 'error', { message: `iOS投屏截图失败:${error.message}` });
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
finally {
|
|
703
|
-
if (state.iosProjection === projection) {
|
|
704
|
-
state.iosProjection = null;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
startIOSNativeMjpegStream(state, projection, host, port, onFallback) {
|
|
709
|
-
const url = `http://${host}:${port}`;
|
|
710
|
-
const { ws } = state;
|
|
711
|
-
return new Promise((resolve) => {
|
|
712
|
-
let settled = false;
|
|
713
|
-
let sentFrames = 0;
|
|
714
|
-
let buffer = Buffer.alloc(0);
|
|
715
|
-
let startupTimer = null;
|
|
716
|
-
const settle = (started) => {
|
|
717
|
-
if (settled)
|
|
718
|
-
return;
|
|
719
|
-
settled = true;
|
|
720
|
-
if (startupTimer)
|
|
721
|
-
clearTimeout(startupTimer);
|
|
722
|
-
resolve(started);
|
|
723
|
-
};
|
|
724
|
-
const req = httpGet(url, (res) => {
|
|
725
|
-
const statusCode = res.statusCode || 0;
|
|
726
|
-
if (statusCode >= 400) {
|
|
727
|
-
res.resume();
|
|
728
|
-
settle(false);
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
res.on('data', (chunk) => {
|
|
732
|
-
if (!this.isSessionOpen(state) || state.iosProjection !== projection || projection.stopped) {
|
|
733
|
-
req.destroy();
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
737
|
-
if (buffer.length > 8 * 1024 * 1024) {
|
|
738
|
-
const lastStart = buffer.lastIndexOf(Buffer.from([0xff, 0xd8]));
|
|
739
|
-
buffer = lastStart >= 0 ? buffer.subarray(lastStart) : Buffer.alloc(0);
|
|
740
|
-
}
|
|
741
|
-
while (true) {
|
|
742
|
-
const start = buffer.indexOf(Buffer.from([0xff, 0xd8]));
|
|
743
|
-
if (start < 0) {
|
|
744
|
-
if (buffer.length > 1024 * 1024)
|
|
745
|
-
buffer = Buffer.alloc(0);
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
const end = buffer.indexOf(Buffer.from([0xff, 0xd9]), start + 2);
|
|
749
|
-
if (end < 0) {
|
|
750
|
-
if (start > 0)
|
|
751
|
-
buffer = buffer.subarray(start);
|
|
752
|
-
break;
|
|
753
|
-
}
|
|
754
|
-
const jpeg = buffer.subarray(start, end + 2);
|
|
755
|
-
buffer = buffer.subarray(end + 2);
|
|
756
|
-
sentFrames += 1;
|
|
757
|
-
if (sentFrames === 1) {
|
|
758
|
-
logger.info(`iOS projection streaming via native WDA MJPEG: ${url}`);
|
|
759
|
-
settle(true);
|
|
760
|
-
}
|
|
761
|
-
this.sendIOSImageFrame(ws, {
|
|
762
|
-
mime: 'image/jpeg',
|
|
763
|
-
base64: jpeg.toString('base64'),
|
|
764
|
-
width: state.videoWidth,
|
|
765
|
-
height: state.videoHeight,
|
|
766
|
-
timestamp: Date.now(),
|
|
767
|
-
source: 'mjpeg',
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
res.on('end', () => {
|
|
772
|
-
if (sentFrames === 0) {
|
|
773
|
-
settle(false);
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
if (this.isSessionOpen(state) && state.iosProjection === projection && !projection.stopped) {
|
|
777
|
-
logger.warn(`iOS native MJPEG ended, falling back to WDA screenshots: ${url}`);
|
|
778
|
-
onFallback();
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
});
|
|
782
|
-
startupTimer = setTimeout(() => {
|
|
783
|
-
req.destroy();
|
|
784
|
-
settle(false);
|
|
785
|
-
}, 1500);
|
|
786
|
-
projection.cleanup = () => req.destroy();
|
|
787
|
-
req.on('error', (error) => {
|
|
788
|
-
if (sentFrames === 0) {
|
|
789
|
-
logger.debug(`iOS native MJPEG unavailable: ${error.message}`);
|
|
790
|
-
settle(false);
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
if (this.isSessionOpen(state) && state.iosProjection === projection && !projection.stopped) {
|
|
794
|
-
logger.warn(`iOS native MJPEG failed, falling back to WDA screenshots: ${error.message}`);
|
|
795
|
-
onFallback();
|
|
796
|
-
}
|
|
797
|
-
});
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
async captureIOSWdaScreenshot(host, port) {
|
|
801
|
-
const controller = new AbortController();
|
|
802
|
-
const timer = setTimeout(() => controller.abort(), 5000);
|
|
803
|
-
try {
|
|
804
|
-
const response = await fetch(`http://${host}:${port}/screenshot`, { signal: controller.signal });
|
|
805
|
-
if (!response.ok) {
|
|
806
|
-
throw new Error(`WDA screenshot HTTP ${response.status}`);
|
|
807
|
-
}
|
|
808
|
-
const body = await response.json();
|
|
809
|
-
const raw = String(body.value || '').replace(/^data:image\/png;base64,/, '');
|
|
810
|
-
if (!raw)
|
|
811
|
-
throw new Error('empty WDA screenshot');
|
|
812
|
-
const buffer = Buffer.from(raw, 'base64');
|
|
813
|
-
const size = this.readPngSize(buffer);
|
|
814
|
-
return { base64: raw, ...size };
|
|
815
|
-
}
|
|
816
|
-
finally {
|
|
817
|
-
clearTimeout(timer);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
readPngSize(buffer) {
|
|
821
|
-
const isPng = buffer.length >= 24 &&
|
|
822
|
-
buffer[0] === 0x89 &&
|
|
823
|
-
buffer[1] === 0x50 &&
|
|
824
|
-
buffer[2] === 0x4e &&
|
|
825
|
-
buffer[3] === 0x47;
|
|
826
|
-
if (!isPng)
|
|
827
|
-
return { width: 0, height: 0 };
|
|
828
|
-
return {
|
|
829
|
-
width: buffer.readUInt32BE(16),
|
|
830
|
-
height: buffer.readUInt32BE(20),
|
|
831
|
-
};
|
|
832
495
|
}
|
|
833
496
|
async connectDeviceForSession(state, options = {}, emitStatus) {
|
|
834
497
|
const { ws } = state;
|
|
@@ -837,16 +500,16 @@ export class ScrcpyServer {
|
|
|
837
500
|
try {
|
|
838
501
|
emitStatus('connecting-device');
|
|
839
502
|
// 重复 connect-device 时,先释放旧资源避免 scrcpy 进程残留。
|
|
840
|
-
if (state.scrcpyClient
|
|
503
|
+
if (state.scrcpyClient) {
|
|
841
504
|
await this.releaseSession(state);
|
|
842
505
|
}
|
|
843
|
-
if (!this.isSessionOpen(state))
|
|
844
|
-
return;
|
|
845
|
-
let devices = await this.getDevicesList();
|
|
846
506
|
if (!this.isSessionOpen(state))
|
|
847
507
|
return;
|
|
848
508
|
let targetDeviceId = resolveRequestedDeviceId(safeOptions, this.currentDeviceId);
|
|
849
509
|
if (!targetDeviceId) {
|
|
510
|
+
const devices = await this.getDevicesList();
|
|
511
|
+
if (!this.isSessionOpen(state))
|
|
512
|
+
return;
|
|
850
513
|
targetDeviceId = this.selectAvailableDeviceId(devices) || undefined;
|
|
851
514
|
}
|
|
852
515
|
if (!targetDeviceId) {
|
|
@@ -870,13 +533,6 @@ export class ScrcpyServer {
|
|
|
870
533
|
this.deviceConnectLocks.set(connectLockKey, state.id);
|
|
871
534
|
this.currentDeviceId = targetDeviceId;
|
|
872
535
|
state.deviceId = targetDeviceId;
|
|
873
|
-
const targetDevice = devices.find((device) => device.id === targetDeviceId);
|
|
874
|
-
const targetPlatform = targetDevice?.platform === 'ios' ? 'ios' : 'android';
|
|
875
|
-
state.platform = targetPlatform;
|
|
876
|
-
if (targetPlatform === 'ios') {
|
|
877
|
-
await this.connectIOSDeviceForSession(state, targetDeviceId, emitStatus);
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
536
|
state.adb = await this.getAdb(targetDeviceId);
|
|
881
537
|
if (!this.isSessionOpen(state)) {
|
|
882
538
|
await this.releaseSession(state);
|
|
@@ -1084,89 +740,6 @@ export class ScrcpyServer {
|
|
|
1084
740
|
metaState: 0,
|
|
1085
741
|
});
|
|
1086
742
|
};
|
|
1087
|
-
const normalizeControlPoint = (raw, width, height) => {
|
|
1088
|
-
let x = Number(raw.x);
|
|
1089
|
-
let y = Number(raw.y);
|
|
1090
|
-
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
1091
|
-
return null;
|
|
1092
|
-
if (raw.normalized === true) {
|
|
1093
|
-
x = Math.max(0, Math.min(1, x)) * width;
|
|
1094
|
-
y = Math.max(0, Math.min(1, y)) * height;
|
|
1095
|
-
}
|
|
1096
|
-
return {
|
|
1097
|
-
x: Math.round(Math.max(0, Math.min(width - 1, x))),
|
|
1098
|
-
y: Math.round(Math.max(0, Math.min(height - 1, y))),
|
|
1099
|
-
};
|
|
1100
|
-
};
|
|
1101
|
-
const ensureIOSProjection = () => {
|
|
1102
|
-
const projection = state.iosProjection;
|
|
1103
|
-
if (!projection || !projection.sessionId) {
|
|
1104
|
-
logger.debug('iOS WDA session not ready, ignoring control event');
|
|
1105
|
-
return null;
|
|
1106
|
-
}
|
|
1107
|
-
return projection;
|
|
1108
|
-
};
|
|
1109
|
-
const handleIOSTouch = async (raw = {}) => {
|
|
1110
|
-
const projection = ensureIOSProjection();
|
|
1111
|
-
if (!projection)
|
|
1112
|
-
return;
|
|
1113
|
-
const width = projection.controlWidth || state.videoWidth || 390;
|
|
1114
|
-
const height = projection.controlHeight || state.videoHeight || 844;
|
|
1115
|
-
const point = normalizeControlPoint(raw, width, height);
|
|
1116
|
-
if (!point)
|
|
1117
|
-
return;
|
|
1118
|
-
const actionStr = String(raw.action || 'down').toLowerCase();
|
|
1119
|
-
if (actionStr === 'down') {
|
|
1120
|
-
projection.touchStart = { ...point, at: Date.now() };
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
if (actionStr !== 'up' && actionStr !== 'cancel') {
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
const start = projection.touchStart || { ...point, at: Date.now() };
|
|
1127
|
-
projection.touchStart = undefined;
|
|
1128
|
-
const distance = Math.hypot(point.x - start.x, point.y - start.y);
|
|
1129
|
-
if (actionStr === 'cancel')
|
|
1130
|
-
return;
|
|
1131
|
-
if (distance < 8) {
|
|
1132
|
-
try {
|
|
1133
|
-
await this.iosWdaRequest(projection.wdaHost, projection.wdaPort, 'POST', `/session/${projection.sessionId}/wda/tap`, { x: point.x, y: point.y });
|
|
1134
|
-
}
|
|
1135
|
-
catch {
|
|
1136
|
-
await this.iosWdaRequest(projection.wdaHost, projection.wdaPort, 'POST', `/session/${projection.sessionId}/wda/tap/0`, { x: point.x, y: point.y });
|
|
1137
|
-
}
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
const duration = Math.max(100, Math.min(1500, Date.now() - start.at));
|
|
1141
|
-
await this.iosWdaRequest(projection.wdaHost, projection.wdaPort, 'POST', `/session/${projection.sessionId}/actions`, {
|
|
1142
|
-
actions: [
|
|
1143
|
-
{
|
|
1144
|
-
type: 'pointer',
|
|
1145
|
-
id: 'finger1',
|
|
1146
|
-
parameters: { pointerType: 'touch' },
|
|
1147
|
-
actions: [
|
|
1148
|
-
{ type: 'pointerMove', duration: 0, x: start.x, y: start.y },
|
|
1149
|
-
{ type: 'pointerDown', button: 0 },
|
|
1150
|
-
{ type: 'pause', duration: 100 },
|
|
1151
|
-
{ type: 'pointerMove', duration, x: point.x, y: point.y },
|
|
1152
|
-
{ type: 'pointerUp', button: 0 },
|
|
1153
|
-
],
|
|
1154
|
-
},
|
|
1155
|
-
],
|
|
1156
|
-
});
|
|
1157
|
-
};
|
|
1158
|
-
const pressIOSButton = async (name) => {
|
|
1159
|
-
const projection = ensureIOSProjection();
|
|
1160
|
-
if (!projection)
|
|
1161
|
-
return;
|
|
1162
|
-
await this.iosWdaRequest(projection.wdaHost, projection.wdaPort, 'POST', `/session/${projection.sessionId}/wda/pressButton`, { name });
|
|
1163
|
-
};
|
|
1164
|
-
const typeIOSText = async (text) => {
|
|
1165
|
-
const projection = ensureIOSProjection();
|
|
1166
|
-
if (!projection || !text)
|
|
1167
|
-
return;
|
|
1168
|
-
await this.iosWdaRequest(projection.wdaHost, projection.wdaPort, 'POST', `/session/${projection.sessionId}/wda/keys`, { value: text.split('') });
|
|
1169
|
-
};
|
|
1170
743
|
const handlers = {
|
|
1171
744
|
ping: () => {
|
|
1172
745
|
wsSend(ws, 'pong');
|
|
@@ -1223,20 +796,22 @@ export class ScrcpyServer {
|
|
|
1223
796
|
},
|
|
1224
797
|
'control-touch': async (raw = {}) => {
|
|
1225
798
|
try {
|
|
1226
|
-
if (state.platform === 'ios') {
|
|
1227
|
-
await handleIOSTouch(raw);
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
799
|
const controller = ensureController();
|
|
1231
800
|
if (!controller)
|
|
1232
801
|
return;
|
|
1233
802
|
const videoWidth = state.videoWidth || 1080;
|
|
1234
803
|
const videoHeight = state.videoHeight || 1920;
|
|
1235
804
|
const normalized = raw.normalized === true;
|
|
1236
|
-
|
|
1237
|
-
|
|
805
|
+
let x = Number(raw.x);
|
|
806
|
+
let y = Number(raw.y);
|
|
807
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
1238
808
|
return;
|
|
1239
|
-
|
|
809
|
+
if (normalized) {
|
|
810
|
+
x = Math.max(0, Math.min(1, x)) * videoWidth;
|
|
811
|
+
y = Math.max(0, Math.min(1, y)) * videoHeight;
|
|
812
|
+
}
|
|
813
|
+
x = Math.round(Math.max(0, Math.min(videoWidth - 1, x)));
|
|
814
|
+
y = Math.round(Math.max(0, Math.min(videoHeight - 1, y)));
|
|
1240
815
|
const actionStr = String(raw.action || 'down').toLowerCase();
|
|
1241
816
|
const action = ACTION_MAP[actionStr] ?? 0;
|
|
1242
817
|
const isUp = actionStr === 'up' || actionStr === 'cancel';
|
|
@@ -1306,11 +881,6 @@ export class ScrcpyServer {
|
|
|
1306
881
|
},
|
|
1307
882
|
'control-text': async (raw = {}) => {
|
|
1308
883
|
try {
|
|
1309
|
-
if (state.platform === 'ios') {
|
|
1310
|
-
const text = typeof raw === 'string' ? raw : String(raw.text ?? '');
|
|
1311
|
-
await typeIOSText(text);
|
|
1312
|
-
return;
|
|
1313
|
-
}
|
|
1314
884
|
const controller = ensureController();
|
|
1315
885
|
if (!controller)
|
|
1316
886
|
return;
|
|
@@ -1342,10 +912,6 @@ export class ScrcpyServer {
|
|
|
1342
912
|
},
|
|
1343
913
|
'control-home': async () => {
|
|
1344
914
|
try {
|
|
1345
|
-
if (state.platform === 'ios') {
|
|
1346
|
-
await pressIOSButton('home');
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
915
|
await pressKey(3);
|
|
1350
916
|
}
|
|
1351
917
|
catch (e) {
|
|
@@ -1380,29 +946,6 @@ export class ScrcpyServer {
|
|
|
1380
946
|
},
|
|
1381
947
|
'control-scroll': async (raw = {}) => {
|
|
1382
948
|
try {
|
|
1383
|
-
if (state.platform === 'ios') {
|
|
1384
|
-
const projection = ensureIOSProjection();
|
|
1385
|
-
if (!projection)
|
|
1386
|
-
return;
|
|
1387
|
-
const width = projection.controlWidth || state.videoWidth || 390;
|
|
1388
|
-
const height = projection.controlHeight || state.videoHeight || 844;
|
|
1389
|
-
const center = normalizeControlPoint({
|
|
1390
|
-
x: raw.x ?? 0.5,
|
|
1391
|
-
y: raw.y ?? 0.5,
|
|
1392
|
-
normalized: raw.normalized !== false,
|
|
1393
|
-
}, width, height);
|
|
1394
|
-
if (!center)
|
|
1395
|
-
return;
|
|
1396
|
-
const dx = Number(raw.scrollX ?? 0) * 120;
|
|
1397
|
-
const dy = Number(raw.scrollY ?? -1) * 160;
|
|
1398
|
-
await handleIOSTouch({ action: 'down', x: center.x, y: center.y });
|
|
1399
|
-
await handleIOSTouch({
|
|
1400
|
-
action: 'up',
|
|
1401
|
-
x: Math.max(0, Math.min(width - 1, center.x - dx)),
|
|
1402
|
-
y: Math.max(0, Math.min(height - 1, center.y - dy)),
|
|
1403
|
-
});
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
949
|
const controller = ensureController();
|
|
1407
950
|
if (!controller)
|
|
1408
951
|
return;
|