Quellcode durchsuchen

触摸模拟完美

yichael vor 5 Monaten
Ursprung
Commit
391a78b37a

+ 85 - 0
main.js

@@ -83,6 +83,91 @@ ipcMain.handle('connect-adb-device', async (event, ipPort) => {
   }
 });
 
+// IPC 处理程序:获取设备分辨率
+ipcMain.handle('get-device-resolution', async (event, ipPort) => {
+  if (!ipPort) {
+    return { success: false, error: '缺少设备 ID' };
+  }
+  try {
+    // 使用 wm size 命令获取设备分辨率
+    const { stdout } = await execAsync(`adb -s ${ipPort} shell wm size`);
+    // 输出格式通常是: "Physical size: 1080x2400" 或 "1080x2400"
+    const match = stdout.match(/(\d+)x(\d+)/);
+    if (match) {
+      return {
+        success: true,
+        width: parseInt(match[1], 10),
+        height: parseInt(match[2], 10)
+      };
+    }
+    // 如果解析失败,返回默认值
+    return { success: true, width: 1280, height: 2400 };
+  } catch (error) {
+    console.error('获取设备分辨率失败:', error);
+    // 返回默认值
+    return { success: true, width: 1280, height: 2400 };
+  }
+});
+
+// IPC 处理程序:抓取设备截屏(返回 base64 PNG)
+ipcMain.handle('capture-screenshot', async (event, ipPort) => {
+  if (!ipPort) {
+    return { success: false, error: '缺少设备 ID' };
+  }
+  try {
+    const { stdout } = await execAsync(`adb -s ${ipPort} exec-out screencap -p`, {
+      encoding: 'buffer',
+      maxBuffer: 25 * 1024 * 1024,
+    });
+    return { success: true, data: stdout.toString('base64') };
+  } catch (error) {
+    console.error('截屏失败:', error);
+    return { success: false, error: error.message };
+  }
+});
+
+// IPC 处理程序:发送 tap 事件到设备
+ipcMain.handle('send-tap', async (event, ipPort, x, y) => {
+  if (!ipPort) {
+    return { success: false, error: '缺少设备 ID' };
+  }
+  if (typeof x !== 'number' || typeof y !== 'number') {
+    return { success: false, error: '坐标必须是数字' };
+  }
+  try {
+    const command = `adb -s ${ipPort} shell input tap ${x} ${y}`;
+    await execAsync(command, {
+      timeout: 5000,
+      maxBuffer: 1024 * 1024
+    });
+    return { success: true };
+  } catch (error) {
+    console.error('Tap 失败:', error.message);
+    return { success: false, error: error.message };
+  }
+});
+
+// IPC 处理程序:发送 swipe 事件到设备
+ipcMain.handle('send-swipe', async (event, ipPort, x1, y1, x2, y2, duration = 300) => {
+  if (!ipPort) {
+    return { success: false, error: '缺少设备 ID' };
+  }
+  if (typeof x1 !== 'number' || typeof y1 !== 'number' || typeof x2 !== 'number' || typeof y2 !== 'number') {
+    return { success: false, error: '坐标必须是数字' };
+  }
+  try {
+    const command = `adb -s ${ipPort} shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`;
+    await execAsync(command, {
+      timeout: 5000,
+      maxBuffer: 1024 * 1024
+    });
+    return { success: true };
+  } catch (error) {
+    console.error('Swipe 失败:', error.message);
+    return { success: false, error: error.message };
+  }
+});
+
 // 应用启动逻辑:设置 CSP、创建窗口、监听激活事件
 app.whenReady().then(() => {
   setContentSecurityPolicy();

+ 4 - 0
preload.cjs

@@ -4,5 +4,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
   scanADBDevices: () => ipcRenderer.invoke('scan-adb-devices'),
   getADBDevices: () => ipcRenderer.invoke('get-adb-devices'),
   connectADBDevice: (ipPort) => ipcRenderer.invoke('connect-adb-device', ipPort),
+  getDeviceResolution: (ipPort) => ipcRenderer.invoke('get-device-resolution', ipPort),
+  captureScreenshot: (ipPort) => ipcRenderer.invoke('capture-screenshot', ipPort),
+  sendTap: (ipPort, x, y) => ipcRenderer.invoke('send-tap', ipPort, x, y),
+  sendSwipe: (ipPort, x1, y1, x2, y2, duration) => ipcRenderer.invoke('send-swipe', ipPort, x1, y1, x2, y2, duration),
 });
 

+ 0 - 0
src/pages/ScreenShot/InptEvent.js


+ 87 - 0
src/pages/ScreenShot/ScrcpyStream.js

@@ -0,0 +1,87 @@
+import { useEffect, useRef, useState, useCallback } from 'react';
+
+// scrcpy 视频流逻辑:使用 scrcpy 协议实现流畅的实时画面
+export function useScrcpyStream(currentDevice) {
+  const [streamUrl, setStreamUrl] = useState(null);
+  const [isStreaming, setIsStreaming] = useState(false);
+  const streamIdRef = useRef(null);
+  const activeDeviceRef = useRef(null);
+
+  // 启动 scrcpy 流
+  const startStream = useCallback(async (device) => {
+    if (!device || !window.electronAPI || !window.electronAPI.startScrcpyStream) {
+      console.warn('Scrcpy API 不可用或设备未连接');
+      return;
+    }
+
+    try {
+      // 停止之前的流(如果有)
+      if (streamIdRef.current) {
+        await stopStream();
+      }
+
+      const result = await window.electronAPI.startScrcpyStream(device);
+      if (result?.success && result?.streamId) {
+        streamIdRef.current = result.streamId;
+        activeDeviceRef.current = device;
+        
+        // 创建 blob URL 用于 video 标签
+        // 注意:实际实现中,streamId 会用于建立 WebSocket 或通过 IPC 传输视频数据
+        // 这里先使用占位符,后续需要根据实际传输方式调整
+        const url = `blob:scrcpy-${result.streamId}`;
+        setStreamUrl(url);
+        setIsStreaming(true);
+      } else {
+        console.error('启动 scrcpy 流失败:', result?.error);
+      }
+    } catch (err) {
+      console.error('启动 scrcpy 流异常:', err);
+    }
+  }, []);
+
+  // 停止 scrcpy 流
+  const stopStream = useCallback(async () => {
+    if (!streamIdRef.current) return;
+
+    try {
+      if (window.electronAPI && window.electronAPI.stopScrcpyStream) {
+        await window.electronAPI.stopScrcpyStream(streamIdRef.current);
+      }
+      
+      // 清理 blob URL
+      if (streamUrl && streamUrl.startsWith('blob:')) {
+        URL.revokeObjectURL(streamUrl);
+      }
+      
+      streamIdRef.current = null;
+      activeDeviceRef.current = null;
+      setStreamUrl(null);
+      setIsStreaming(false);
+    } catch (err) {
+      console.error('停止 scrcpy 流异常:', err);
+    }
+  }, [streamUrl]);
+
+  // 监听设备变化
+  useEffect(() => {
+    if (currentDevice && currentDevice !== activeDeviceRef.current) {
+      startStream(currentDevice);
+    } else if (!currentDevice && activeDeviceRef.current) {
+      stopStream();
+    }
+
+    return () => {
+      if (activeDeviceRef.current) {
+        stopStream();
+      }
+    };
+  }, [currentDevice, startStream, stopStream]);
+
+  return {
+    streamUrl,
+    isStreaming,
+    startStream,
+    stopStream,
+  };
+}
+

+ 6 - 5
src/pages/ScreenShot/ScreenShot.js

@@ -31,6 +31,7 @@ export function ScreenShotLogic() {
           return;
         }
 
+        // 请求截图
         const res = await window.electronAPI.captureScreenshot(device);
         
         if (!res?.success || !res.data) {
@@ -38,7 +39,7 @@ export function ScreenShotLogic() {
           await delay(400);
           runOnce();
         } else {
-          // 截图成功,更新图片并等待加载完成
+          // 截图成功,更新图片
           const stamp = frameCounterRef.current++;
           const dataUrl = `data:image/png;base64,${res.data}#${stamp}`;
           
@@ -47,13 +48,13 @@ export function ScreenShotLogic() {
             waitForLoadRef.current = resolve;
           });
           
-          // 更新图片源
+          // 更新图片源(会触发图片加载)
           setImageSrc(dataUrl);
           
-          // 等待图片加载完成(onLoad 触发)或超时(1秒),然后请求下一帧
-          await Promise.race([waitPromise, delay(1000)]);
+          // 等待图片加载完成(onLoad 触发)或超时(500ms),然后立即请求下一帧
+          await Promise.race([waitPromise, delay(500)]);
           
-          // 继续下一帧
+          // 继续请求下一帧截图
           runOnce();
         }
       } catch (err) {

+ 69 - 13
src/pages/ScreenShot/TouchEvent.js

@@ -1,8 +1,4 @@
-import { useRef, useCallback } from 'react';
-
-// 设备实际分辨率(竖屏)
-const DEVICE_WIDTH = 1280;
-const DEVICE_HEIGHT = 2400;
+import { useRef, useCallback, useState, useEffect } from 'react';
 
 // 触摸事件处理逻辑:坐标转换和手势模拟
 export function useTouchEvents(currentDevice, imageRef) {
@@ -10,6 +6,30 @@ export function useTouchEvents(currentDevice, imageRef) {
   const startPos = useRef({ x: 0, y: 0 });
   const lastPos = useRef({ x: 0, y: 0 });
   const touchStartTime = useRef(0);
+  const [deviceResolution, setDeviceResolution] = useState({ width: 1280, height: 2400 });
+
+  // 获取设备实际分辨率
+  useEffect(() => {
+    const fetchDeviceResolution = async () => {
+      if (!currentDevice || !window.electronAPI || !window.electronAPI.getDeviceResolution) {
+        // 如果 API 不可用,使用默认值
+        return;
+      }
+
+      try {
+        const result = await window.electronAPI.getDeviceResolution(currentDevice);
+        if (result?.success && result.width && result.height) {
+          setDeviceResolution({ width: result.width, height: result.height });
+        }
+      } catch (err) {
+        console.warn('获取设备分辨率失败,使用默认值:', err);
+      }
+    };
+
+    if (currentDevice) {
+      fetchDeviceResolution();
+    }
+  }, [currentDevice]);
 
   // 将鼠标坐标转换为设备坐标
   const convertToDeviceCoords = useCallback((clientX, clientY) => {
@@ -20,20 +40,56 @@ export function useTouchEvents(currentDevice, imageRef) {
     const img = imageRef.current;
     const rect = img.getBoundingClientRect();
     
-    // 计算鼠标相对于图片的位置(0-1 范围)
-    const relativeX = (clientX - rect.left) / rect.width;
-    const relativeY = (clientY - rect.top) / rect.height;
+    // 获取图片的实际显示尺寸(考虑 object-fit: contain)
+    // 图片可能不会完全填满容器,需要计算实际显示区域
+    let displayWidth, displayHeight, offsetX, offsetY;
+    
+    if (img.naturalWidth && img.naturalHeight) {
+      // 获取图片的原始尺寸
+      const imgAspect = img.naturalWidth / img.naturalHeight;
+      const containerAspect = rect.width / rect.height;
+      
+      if (imgAspect > containerAspect) {
+        // 图片更宽,以宽度为准(左右可能有黑边)
+        displayWidth = rect.width;
+        displayHeight = rect.width / imgAspect;
+        offsetX = 0;
+        offsetY = (rect.height - displayHeight) / 2;
+      } else {
+        // 图片更高,以高度为准(上下可能有黑边)
+        displayHeight = rect.height;
+        displayWidth = rect.height * imgAspect;
+        offsetX = (rect.width - displayWidth) / 2;
+        offsetY = 0;
+      }
+    } else {
+      // 如果图片尺寸未知,使用容器尺寸
+      displayWidth = rect.width;
+      displayHeight = rect.height;
+      offsetX = 0;
+      offsetY = 0;
+    }
+    
+    // 计算鼠标相对于实际显示区域的位置
+    const relativeX = (clientX - rect.left - offsetX) / displayWidth;
+    const relativeY = (clientY - rect.top - offsetY) / displayHeight;
+    
+    // 检查是否在有效显示区域内(0-1之间)
+    if (relativeX < 0 || relativeX > 1 || relativeY < 0 || relativeY > 1) {
+      // 点击在显示区域外(黑边区域),返回 null
+      return null;
+    }
     
     // 转换为设备坐标
-    const deviceX = Math.round(relativeX * DEVICE_WIDTH);
-    const deviceY = Math.round(relativeY * DEVICE_HEIGHT);
+    const deviceX = Math.round(relativeX * deviceResolution.width);
+    const deviceY = Math.round(relativeY * deviceResolution.height);
     
     // 确保坐标在设备范围内
-    const clampedX = Math.max(0, Math.min(DEVICE_WIDTH - 1, deviceX));
-    const clampedY = Math.max(0, Math.min(DEVICE_HEIGHT - 1, deviceY));
+    const clampedX = Math.max(0, Math.min(deviceResolution.width - 1, deviceX));
+    const clampedY = Math.max(0, Math.min(deviceResolution.height - 1, deviceY));
     
     return { x: clampedX, y: clampedY };
-  }, [imageRef]);
+  }, [imageRef, deviceResolution]);
 
   // 发送 tap 事件到设备
   const sendTap = useCallback(async (x, y) => {