首页 > 其他分享 >Android网页投屏控制从入门到放弃

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

时间:2024-08-11 22:38:29浏览次数:12  
标签:const parameters err 投屏 adb diff 网页 Android diffData

背景

业务需要采集在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.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方案,在这个热闹的周末,正好闲暇的时间,了解过去不曾接触的知识,也是一个有趣的过程。

标签:const,parameters,err,投屏,adb,diff,网页,Android,diffData
From: https://www.cnblogs.com/xiaoqi/p/18354023/web-scp

相关文章

  • Android Studio开发学习(八、蓝牙设备连接)
    引言        上篇我们介绍了整体蓝牙模块的基本写法AndroidStudio开发学习(七、蓝牙模块java)-CSDN博客,当然我们检测到蓝牙设备肯定会连接蓝牙,那么如何实现蓝牙连接呢,这篇博客将实现这个功能。 设备连接        connectDevice 方法用于在后台线程中尝试连......
  • RouterOS设置IPV6并解决部分网页打开慢和无法打开问题
    目前ADSL各大运营商已完成IPv6的部署,很多网站和应用也开始部署IPv6,早先尝试过启用IPv6,但是出现部分网站打不开的情况,一直未找到原因,最近终于发现了问题所在,终于能正常使用IPv6了。WinBox下启用IPv6的步骤:1、启用IPv6软件包:System=>Packages,将ipv6设置为enable2、获取IP......
  • Android 13.0 禁用adb install 安装app功能
    1.前言 在13.0的系统rom产品开发中,在进行一些定制开发中,对于一些app需要通过属性来控制禁止安装,比如adbinstall也不允许安装,所以就需要熟悉adbinstall的安装流程,然后来禁用adbinstall安装功能,接下来分析下adb下的安装流程,来实现相关的功能2.禁用adbinstall安装app功......
  • Android14音频进阶调试之命令播放mp3/aac非裸流音频(八十)
    简介:CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长!新书发布:《Android系统多媒体进阶实战》......
  • Android Media Framework(十六)ACodec - Ⅳ
    上一篇文章讲了ACodec的buffer分配过程,有同学读完后提出了一些问题,所以本篇内容是对上一篇文章的补充。本篇内容将会了解GraphicOutputBufferCount的计算方式,复习useBuffer的调用,学习ACodecBufferChannel。1、configureOutputBuffersFromNativeWindow有同学提问使用N......
  • Android Media Framework(十七)ACodec - Ⅴ
    本篇文章我们一起来分析Executing状态下的数据处理流程。首先对上一篇文章做勘误:实际在调用allocateOutputBuffersFromNativeWindow分配buffer时我们会看到,一开始确实是分配了nBufferCountActual个buffer,但是后面又调用cancelBufferToNativeWindow销毁掉了备用的。可能......
  • 腾讯地图SDK Android版开发 3 地图图层
    腾讯地图SDKAndroid版开发3地图图层前言腾讯地图图层地图底图类型地图类图层类型常量接口路况图层接口示例代码地图风格类地图底图类型实时路况页面布局控件响应事件地图底图类型实时路况运行效果图前言本文主要介绍腾讯地图图层相关的功能和接口,以及使用方......
  • Java计算机毕业设计基于Android的校园网上拍卖平台(开题报告+源码+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景在数字化校园建设的浪潮中,学生们对于便捷、高效的二手商品交易需求日益增长。传统的校园跳蚤市场受限于时间、空间等因素,难以满足学生群体对于多样化......
  • Java计算机毕业设计基于Android的生活记账小助手APP的设计与实现(开题报告+源码+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景在快节奏的现代生活中,个人财务管理成为了许多人面临的一大挑战。随着智能手机的普及和移动互联网技术的飞速发展,移动应用成为辅助个人财务管理的得力......
  • 秒开WebView?Android性能优化全攻略
    在Android应用中,WebView组件被广泛用于显示网页内容。然而,由于WebView的一些固有限制和资源消耗,它可能会导致应用启动变慢或响应速度下降。下面是一些优化WebView性能的策略,以帮助你实现“秒开”效果:1.减少初始化时间• 延迟加载:不要在应用启动时立即初始化WebView,......