Android网页投屏控制从入门到放弃

背景

业务需要采集在app上执行任务的整个过程,原始方案相对复杂,修改需要协调多方人员,因而考虑是否有更轻量级的方案。

原始需求:

  • 记录完成任务的每一步操作(点击、滑动、输入等)
  • 记录操作前后的截图和布局xml

基于Adb的方案

最容易考虑到的方案是就是通过adb去实现,要获取到当前页面的xml、当前页面截图,所以只需要将每一步操作通过adb发送给手机端即可。

步骤

  1. 通过adb连接设备,编写一个agent程序接收网页操作请求,并通过adb发送指令执行
  2. adb获取当前页面xml(uiautomator dump)
  3. adb获取当前页面截图(screencap),agent通过ws发送到网页端
  4. 网页显示图片,监控鼠标点击事件,计算出点击位置
  5. 将相关操作通过adb发送到设备,模拟操作
  6. 循环步骤2-5

弄清楚流程,可以直接告诉编程LLM,代码秒成,考虑到golang依赖较少,我们直接让LLM生成golang代码。

下面介绍部分实现,比如golang调用adb,网页端传入deviceid和操作:


func executeCommand(deviceID string, action string, parameters string) error {cmdArgs := []string{"-s", deviceID, "shell", action}if parameters != "" {cmdArgs = append(cmdArgs, parameters)}fmt.Println(cmdArgs)cmd := exec.Command("adb", cmdArgs...)err := cmd.Run()if err != nil {return err}return nil
}

比如截图,调用screencap截取png格式的图片:

func screenshot(deviceID string) ([]byte, error) {cmd := exec.Command("adb", "-s", deviceID, "exec-out", "screencap", "-p")var out bytes.Buffercmd.Stdout = &outerr := cmd.Run()if err != nil {return nil, err}return out.Bytes(), nil
}

JavaScript端显示:

socket.onmessage = (event) => {if (event.data instanceof Blob) {                   const url = URL.createObjectURL(event.data);imgElement.src = url;}
}

图片上方可以加一个div层,用来监控鼠标事件,模拟操作:

overlayElement.addEventListener('mousedown', (e) => {startX = e.offsetX;startY = e.offsetY;startTime = Date.now();
});overlayElement.addEventListener('mouseup', (e) => {const endX = e.offsetX;const endY = e.offsetY;const elapsedTime = Date.now() - startTime;const duration = Math.max(elapsedTime / 1000, 0.001); // Avoid zero divisionconst imgStartX = (startX / imgDisplayWidth) * imgWidth;const imgStartY = (startY / imgDisplayHeight) * imgHeight;const imgEndX = (endX / imgDisplayWidth) * imgWidth;const imgEndY = (endY / imgDisplayHeight) * imgHeight;if (Math.abs(imgStartX - imgEndX) > 5 || Math.abs(imgStartY - imgEndY) > 5) {sendCommand('input swipe', `${imgStartX} ${imgStartY} ${imgEndX} ${imgEndY}`);}else if (duration > 500) {// 长按sendCommand('input swipe', `${imgStartX} ${imgStartY} ${imgEndX} ${imgEndY} ${duration / 1000}`);}else {sendCommand('input tap', `${imgStartX} ${imgStartY}`);}
});

效果与问题

效果如下:

问题也很多:

  • screencap 比较慢,测试模拟器需要600~700ms,显示起来感觉比较卡顿
  • 大部分时候,页面没操作,图片基本不变化,重复传输浪费网络
  • uiautomator dump 更夸张,2~3s

优化

图像差分传输,截图后检查下是否变化,没有变化就不发送,有变化就发送diff图像,这样JavaScript端合并图像就可以了。

diff 用最简单的策略,相同的改为全透明,不同的保留原图像,计算diff图:

// CalculateDifference 计算两个RGBA图像之间的差异, 并返回新的RGBA图像
// 如果两个图片完全一致,则返回全透明的图像
func CalculateDifference(img1, img2 image.Image) *image.NRGBA {bounds := img1.Bounds()diff := image.NewNRGBA(bounds)for y := bounds.Min.Y; y < bounds.Max.Y; y++ {for x := bounds.Min.X; x < bounds.Max.X; x++ {c1 := img1.At(x, y).(color.NRGBA)c2 := img2.At(x, y).(color.NRGBA)if c1 == c2 {diff.Set(x, y, color.NRGBA{}) // 完全一致时,设置为全0continue} else {diff.Set(x, y, c2)}// 组合RGB和Alpha通道为一个16位灰度值(分开存储Alpha通道可能更实际)}}return diff
}

Javascrit 接收到后可以结合上一张图进行还原,前端可以用canvas去操作diff图像进行合并:


function createImageFromBlob(blob) {return new Promise((resolve, reject) => {const img = new Image();img.onload = () => resolve(img);img.onerror = reject;img.src = URL.createObjectURL(blob);});
}async function restoreImage(diffImageBlob) {const refImage = imgElement;const diffImage = await createImageFromBlob(diffImageBlob);canvas.width = refImage.width;canvas.height = refImage.height;ctx.drawImage(refImage, 0, 0);const refImageData = ctx.getImageData(0, 0, refImage.width, refImage.height);ctx.drawImage(diffImage, 0, 0);const diffImageData = ctx.getImageData(0, 0, diffImage.width, diffImage.height);const resultImageData = ctx.createImageData(refImage.width, refImage.height);const refData = refImageData.data;const diffData = diffImageData.data;const resultData = resultImageData.data;for (let i = 0; i < refData.length; i += 4) {// Assuming diff is non-zero means it contains the correct pixelresultData[i] = diffData[i] !== 0 ? diffData[i] : refData[i];       // RresultData[i + 1] = diffData[i + 1] !== 0 ? diffData[i + 1] : refData[i + 1]; // GresultData[i + 2] = diffData[i + 2] !== 0 ? diffData[i + 2] : refData[i + 2]; // BresultData[i + 3] = diffData[i + 3] !== 0 ? diffData[i + 3] : refData[i + 3]; // A}ctx.putImageData(resultImageData, 0, 0);
}

结论

虽然思路可行,但是因为adb 截图和获取xml比较慢,最终方案用不了,只能换一个思路去解决。

基于uiautomator2的方案

uiautomator2 是一个python库,用python调用设备上uiautomator服务来获取页面信息、控制设备,其原理也比较简单,就是通过adb在设备上启动atxagent和server等程序,然后通过http和ws去连接设备从而实现控制。

uiautomator2可以几十ms的时间获取xml,截图也因为高效的minicap,可以提供更高的fps。

使用uiautomator2就需要将golang转成python,幸运的是直接扔给LLM,先转成python,然后让将使用adb的改成使用uiautomator2,基本上大差不差,稍微缝缝补补搞定。
这里要感慨,LLM对程序员真是好助手,做好方案设计,扔给LLM就能比较好的去实现,心有灵犀。(PS: 胸中有丘壑,LLM才是好助手)

讲一些改动, 执行命令,可以提供一个更通用的方法,方便前端直接调用:

async def execute_command(device_id, action, parameters):try:device = get_device(device_id)command = getattr(device, action)if parameters:command(**parameters)else:command()except Exception as e:print(f"Error executing command on device {device_id}: {e}")

JS 调用

sendCommand('click', {"x": imgStartX, "y": imgStartY});async function sendCommand(action, parameters) {       const command = JSON.stringify({ type: 'action', deviceID: deviceID, action: action, parameters: parameters });socket.send(command);console.log('Command:', command);
}

优化

模拟click按键,发送按键事件:

// SendKeyEvent 按下一个键(字符或功能键)
func (d *Driver) SendKeyEvent(keyCode string) error {cmd := exec.Command("adb", d.deviceID, "shell", "input", "keyevent", keyCode)err := cmd.Run()if err != nil {return err}return nil
}

准实时投屏的方案

上面采用的minicap,截图已经很快了,一秒钟传输几张图片,基本上满足这个场景够用了。 还有更准实时的方案吗?

专业的开源投屏控制软件 scrpy 是一个好的选择,scrpy实现原理其实类似上面的uiautomator2,会在device上启动一个server,通过server获取音视频流,以及控制。
scrpy 技术上相对更成熟,而uiautomator2依赖的minicap则缺乏维护,对安卓新版本支持不够好。

所以,在获取截图方面,也可以考虑调用scrpy的server来实现准实时控制。但是就如标题所说,从入门到放弃,上面的方案已经可以满足我们需求,没必要在这里投入更多的精力,所以这个方案放弃。

结语

本文主要记录投屏控制相关的实践过程,通过从adb方案开始,到uiautomator2,以及最后放弃scrpy方案,在这个热闹的周末,正好闲暇的时间,了解过去不曾接触的知识,也是一个有趣的过程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/781465.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法学习:矩阵快速幂/矩阵加速

1.前言其实本质上来说,矩阵快速幂或是矩阵加速的题目比较的模板化一些,大体上都是属于我们要先写出来一个递推式子(或者是我们需要递推的式子),然后由于递推的次数过大,1e18之类的,会导致复杂度的飚升,所以我们会用到矩阵来帮我们快速处理。另外,从题目的类型上大概是…

web渗透—RCE

一:代码执行相关函数1、eval()函数 assert()函数 (1)原理:将用户提交或者传递的字符串当作php代码执行 (2)passby:单引号绕过:闭合+注释;开启GPC的话就无法绕过(GPC就是将单引号转换为"反斜杠+单引号") eg: <?phphighlight_file(__FILE__);//高亮显示代码$…

代码块

代码块 概述:在Java中,使用{}括起来的代码被称为代码块,根据其位置和声明的不同,可以分为局部代码块,构造代码块,静态代码块,同步代码块(多线程讲解)。 (1)局部代码块 在方法中出现;限定变量生命周期,及早释放,提高内存利用率 (2)构造代码块 在类中方法外出现;多…

【书生浦语大模型实战营学习笔记】第一课 浦语大模型全链路开源开放体系

视频内容总结: 视频是由汪周谦主讲, 主题是介绍书生谱语大模型开源开放体系。内容主要包括以下几个方面: 1. **书生谱语大模型的发展历程**: - 从2023年7月6日起,书生谱语大模型(Interlm)开始免费开源并商用,提供了全链条的开源工具体系。 - 2023年9月底,发布了适…

u8g2字体库命名规则及符号库的使用

u8g2字体命名规则 <prefix> _ <name> _ <purpose> <char set> prefix:基本上都是 u8g2;name:一般会挂钩上字符像素使用量,比如5X7purpose: t(transparent)\h(height)\m(monospace)\8(8x8pixe)<purpose> Descriptiont Transparent font, Do not…

Python 虚拟环境安装flask框架 Read timed out.

cmd 输入workon env_name 激活 pip install flask 升级pip: python -m pip install --upgrade pip -i http://pypi.douban.com/simple --trusted-host pypi.douban.com 下载包:以flask-sqlalchemy为例: 方案一:pip install Flask 方案二:pip install flask-sqlalchemy -i…

web渗透—sql注入

一:union联合注入 1、万能密码and优先级高于or,先执行and; 则username = "用户提交" and password ="1"为假;or后面的条件恒为真;则where条件为真;输出admin表中所有的信息select * from admin where username = "用户提交" and password =…

一个好用的消息推送服务【Server 酱】

今天给大家介绍一个好用的消息推送服务Server 酱 Server 酱简介 Server 酱是什么 「Server 酱」,英文名「ServerChan」,是一款「手机」和「服务器」、「智能设备」之间的通信软件。 说人话?就是从服务器、路由器等设备上推消息到手机的工具。 开通并使用上它,只需要一分钟:…

数据接口安全风险监测技术

数据接口: 信息系统之间进行数据传输和交换的一种机制,它描述了一个由接口服务端和客户端端共同遵守的合约,通常会约定数据的格式、通信协议、传输结构等。 风险源: 可能导致危害数据的保密性、完整性、可用性和数据处理合理性等事件的威胁、脆弱性、问题、隐患等。 一、数…

USB协议详解第6讲(USB描述符-端点描述符)

1.USB描述符 USB描述符有设备描述符、标准配置描述符、接口描述符、端点描述符、字符串描述符,HID设备有HID描述符、报告描述符和物理描述符。今天主要是学习USB端点描述符的组成。 2.端点描述符组成 前面讲了设备描述符、标准配置描述符、接口描述符,本篇我们讲解端点描述符…

【Web API 】渗透测试指南

一、概述1.1 API的基本概念1.2 API的作用1.3 API的类型二、Web API 渗透测试2.1 测试工具2.2 信息收集2.2.1 目录扫描2.2.2 网络流量分析2.2.3 使用互联网资源2.3 漏洞检测2.4 实战案例2.4.1 接口枚举2.4.2 参数枚举2.4.3 用户名枚举2.4.4 暴力破解2.4.5 错误信息泄露2.4.6 CRL…