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 ...
随机推荐
- 【iOS】push控制器时隐藏tabbar,dismiss控制器时显示tabbar
在push之前将控制器的属性hidesBottomBarWhenPushed设置为yes就好. //准备要把控制器vc给push出去了 UIViewController *vc = [[UIViewC ...
- Windows无法调节亮度
原因1:驱动问题 解决方式: 安装360驱动大师,一键安装. 也可以使用其他软件:如驱动精灵. 推荐使用电脑品牌本身的驱动软件:如联想:联想驱动管理 原因2:设备管理问题 解决方式: 计算机 -> ...
- QT6设置应用程序图标
准备好一个ico格式的图标, 放到源码文件中, 比如放在 resources/logo.ico 在源码目录中新建一个icon.rc的文件, 内容如下: IDI_ICON1 ICON DISCARDAB ...
- 新品来袭,全国产ARM+FPGA--"RK3568J+Logos-2"工业核心板,让您的硬件设计“更简单”!
如需选购,请登录创龙科技天猫旗舰店: tronlong.tmall.com! 欢迎加入RK3568J技术交流群:567208221 欢迎加入Logos-2技术交流群:311416997 更多产品详情以 ...
- 全志科技T507-H工业核心板规格书(4核ARM Cortex-A53,主频1.416GHz)
1 核心板简介 创龙科技SOM-TLT507是一款基于全志科技T507-H处理器设计的4核ARM Cortex-A53全国产工业核心板,主频高达1.416GHz.核心板CPU.ROM.RAM.电源.晶 ...
- 阿里云服务器Docket安装RabbitMQ 3.8.12
DocketMQ安装RabbitMQ 地址:https://hub.docker.com/ 拉取镜像 docker pull rabbitmq:3.8.12-management-alpine 运行 ...
- yb课堂 VueCli 4.3搭建yb课堂前端项目架构 《三十二》
使用VueCli 4.3搭建yb课堂前端项目框架 创建yb课堂Vue项目 vue create ybclass_front 选择feature模式 安装vuex.vue-router,用vscode打 ...
- Oracle 递归拼接字段
效果 sql SELECT LISTAGG(T.NAME, ' / ') WITHIN GROUP(ORDER BY LEVEL DESC) AS RESULT FROM S_WORK_RESOURS ...
- 洛谷P5020
水一道绿题... #include<iostream> #include<utility> #include<algorithm> using namespace ...
- 今天我们来聊Java IO模型,BIO、NIO、AIO三种常见IO模型
一.写在开头 很久没更新喽,最近build哥一直在忙着工作,忙着写小说,都忘记学习自己的本职了,哈哈,不过现在正式回归! 我们继续学习Java的IO相关内容,之前我们了解到,所谓的IO(Input/O ...