Android网页投屏控制从入门到放弃
背景
业务需要采集在app上执行任务的整个过程,原始方案相对复杂,修改需要协调多方人员,因而考虑是否有更轻量级的方案。
原始需求:
- 记录完成任务的每一步操作(点击、滑动、输入等)
- 记录操作前后的截图和布局xml
基于Adb的方案
最容易考虑到的方案是就是通过adb去实现,要获取到当前页面的xml、当前页面截图,所以只需要将每一步操作通过adb发送给手机端即可。
步骤
- 通过adb连接设备,编写一个agent程序接收网页操作请求,并通过adb发送指令执行
- adb获取当前页面xml(uiautomator dump)
- adb获取当前页面截图(screencap),agent通过ws发送到网页端
- 网页显示图片,监控鼠标点击事件,计算出点击位置
- 将相关操作通过adb发送到设备,模拟操作
- 循环步骤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.Buffer
cmd.Stdout = &out
err := 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 division
const 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{}) // 完全一致时,设置为全0
continue
} 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 pixel
resultData[i] = diffData[i] !== 0 ? diffData[i] : refData[i]; // R
resultData[i + 1] = diffData[i + 1] !== 0 ? diffData[i + 1] : refData[i + 1]; // G
resultData[i + 2] = diffData[i + 2] !== 0 ? diffData[i + 2] : refData[i + 2]; // B
resultData[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方案,在这个热闹的周末,正好闲暇的时间,了解过去不曾接触的知识,也是一个有趣的过程。
Android网页投屏控制从入门到放弃的更多相关文章
- Android 电脑投屏工具Vysor Pro介绍
Chrome的插件,直接到chrome的扩展程序里面搜索Vysor,安装即可 如何破解: C:\Users\lanlan.shi\AppData\Local\Google\Chrome\User Da ...
- 各手机PC品牌投屏功能连接方法
一.iOS终端(iPhone/iPad)无线投屏: 1.将iPhone或iPad与必捷会议盒子连接至同一路由器: 2.滑动iPhone/iPad的屏幕,调出Airplay功能,选择需要投屏的主机,开始 ...
- 海豚星空扫码投屏 Android 接收端 SDK 集成 六步骤
一 跟目录的build.gradle添加私有mevan仓库 maven {url 'http://nexus.dolphinstar.cn/repo/openmavenx'} 二 app/build. ...
- Ubuntu安装scrcpy手机投屏和控制(Ubuntu用QQ微信的另一种方法)
Scrcpy 安装 snap install scrcpy adb服务安装 sudo apt-get install android-tools-adb adb配置 查看手机的USB识别号 手机通过U ...
- 同时支持Android 和 ios 投屏到电脑的软件,Support Android and ios screen shrare to PC - 希沃授课助手
最近学校由粉笔黑板更换了智慧电子黑板,然后发现了一个好玩的软件. 感谢希沃公司的开发: 希沃授课助手,这是一款同时支持Android 和 ios 投屏和远程控制的. 效果很流畅,非常赞
- scrcpy投屏android手机到电脑
在mac os下 投票iPhone投屏是最简单不过了,只需要用Quicktime player就可以. 但是在mac下咱投屏android的手机呢,就需要用到scrcpy了. 1.打开终端,输入命令: ...
- 看完小白也会使用,Android投屏神器scrcpy详细教程
楔子 做为一个软件测试工程师,在使用手机测试的时候,缺陷附件想附上截图.视频,需要从手机把图片.视频发送到拷贝或发送到电脑,非常麻烦. 所以想到使用投屏软件,把手机的屏幕投屏到电脑,便可以直接在电脑上 ...
- ios11苹果手机怎么投屏到电脑
使用过苹果手机的用户都知道,苹果手机触摸屏操作极为流畅,网页浏览也非常轻松,各种网络上的应用可以说是非常完美.iPhone的娱乐功能相当的强大,能让苹果iPhone超越了其他手机很大的距离.但是手机怎 ...
- QML - 实现Gstreamer投屏 投屏画面遮挡
1. 背景介绍 中控端运行的操作系统是Android,中控软件主要功能有导航.收音机.媒体(音乐).蓝牙(连接).手机互联.行车辅助和系统设置等. 仪表端运行的操作系统是Linux,仪表软件主 ...
- 分享一个开源的windows安卓投屏工具,scrcpy
看到scrcpy可能很多人会以为是大名鼎鼎的Scrcpy(一个十分强大的多线路爬虫框架),sorry今天分享的主角不是他,而是他: github地址:https://github.com/Genymo ...
随机推荐
- Docker PHP启用各种扩展笔记
注意 如果apt-get install命令无法安装依赖,请先执行apt update更新依赖信息 启用ZIP扩展 原作者地址:找不到了... # 安装依赖库 $ apt-get install -y ...
- Linux信号量
查看信号量 [root@localhost ~]# kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) ...
- 19-Docker数据持久化
什么是Docker数据持久化 容器在运行时会在镜像层上加上一层:可写层. 当删除容器时,可写层就会一起被删除,数据丢失. 数据持久化就是就是将数据持久化保存,删除容器之后,数据仍然存在. 方法1-挂载 ...
- Codeforces Round 894 (Div. 3) A-E cd 894 div3
A. Gift Carpet 每道题都是伸缩代码框有ac代码请不要漏掉 --------------------------题解----------------------------- 按先行便然后 ...
- 虚拟 DOM 的优缺点?
什么是虚拟dom用js模拟一颗dom树,放在浏览器内存中.当你要变更时,虚拟dom使用diff算法进行新旧虚拟dom的比较,将变更放到变更队列中, 反应到实际的dom树,减少了dom操作. 虚拟DOM ...
- 使用Sequelize
访问MySQL 当我们安装好MySQL后,Node.js程序如何访问MySQL数据库呢? 访问MySQL数据库只有一种方法,就是通过网络发送SQL命令,然后,MySQL服务器执行后返回结果. 我们可以 ...
- 解决方案 | vb记住上次打开的文件夹
Private Sub Button_ImportBasicData_Click(sender As Object, e As EventArgs) Handles Button_ImportBa ...
- Curve 替换 Ceph 在网易云音乐的实践
Curve 块存储已在生产环境上线使用近三年,经受住了各种异常和极端场景的考验,性能和稳定性均超出核心业务需求预期 网易云音乐背景 网易云音乐是中国领先的在线音乐平台之一,为音乐爱好者提供互动的内容社 ...
- oeasy教您玩转python - 007 - # 字符本质
字符本质 回忆上次内容 hello world 不是从来就有的 来自于unix和c 虽然我们今天有各种先进的学习手段 最早的高级语言学习是从最早的那张打字机用纸的手写代码起源的 所以输出用的是 p ...
- njs最详细的入门手册:Nginx JavaScript Engine
原文链接:https://hi.imzlh.top/2024/07/08.cgi 关于njs 首先,njs似乎在国内外都不受关注,资料什么的只有 官网参考手册,出了个问题只能看到Github Issu ...