这是一个在自动化测试、UI 测试、机器人流程自动化等领域非常强大的技术组合。
核心概念
- Minicap: 一个由 OpenStadio 团队开发的开源项目,它将 Android 设备的屏幕帧实时地以视频流的形式推送到一个 TCP 端口,你的 Python 客户端可以连接到这个端口,接收并解码每一帧图像,从而实现屏幕录制或实时获取屏幕截图。
- Minitouch: 与 Minicap 配套,它允许你通过网络向设备发送触摸事件(按下、移动、抬起),这使得你可以将录制的触摸轨迹“回放”到设备上,实现自动化操作。
- ADB (Android Debug Bridge): 是我们与 Android 设备通信的基础,我们需要用它来安装
minicap和minitouch的服务端程序,并启动它们。
第一步:环境准备
安装 ADB
确保你的电脑上安装了 ADB 并且可以识别你的 Android 设备。
# 检查设备是否连接 adb devices # 如果设备未授权,请在设备上弹出授权对话框并确认
准备 Minicap 和 Minitouch 服务端
你需要为你的 Android 设备下载对应架构的 minicap 和 minitouch 可执行文件。
-
获取设备架构:
adb shell getprop ro.product.cpu.abi
常见的架构有
armeabi-v7a,arm64-v8a,x86,x86_64。 -
下载服务端文件 你可以从 stf-binaries 仓库下载对应架构的文件,这里我们以
arm64-v8a为例。- 在你的电脑上创建一个临时目录,
minicap_files。 - 下载以下三个文件到该目录:
minicap(可执行文件)minicap.so(共享库)minitouch(可执行文件)
- 在你的电脑上创建一个临时目录,
-
推送并设置权限 将这些文件推送到设备的
/data/local/tmp目录,并赋予可执行权限。# 假设你的设备架构是 arm64-v8a adb push minicap /data/local/tmp/ adb push minicap.so /data/local/tmp/lib/android-23/arm64-v8a/ adb push minitouch /data/local/tmp/ adb shell chmod 755 /data/local/tmp/minicap adb shell chmod 755 /data/local/tmp/minitouch
安装 Python 依赖
在你的 Python 环境中安装必要的库。
pip install opencv-python numpy pillow
opencv-python: 用于高效地处理视频流、解码图像、保存视频文件。numpy: OpenCV 内部依赖,也用于图像数据处理。pillow: 有时用于更简单的图像处理。
第二步:Python 实现
我们将创建两个主要的 Python 脚本:
screen_recorder.py: 用于录制屏幕。touch_replayer.py: 用于回放触摸事件。
脚本 1: screen_recorder.py (录制)
这个脚本会启动 minicap 服务,连接它,并将接收到的帧保存为 MP4 文件。
import cv2
import numpy as np
import subprocess
import socket
import time
import os
# --- 配置 ---
DEVICE_ID = "your_device_id" # adb devices -l 中的 id,通常为空
DEVICE_ARCH = "arm64-v8a" # 你的设备架构
MINICAP_PATH = "/data/local/tmp/minicap"
OUTPUT_VIDEO = "screen_recording.mp4"
FPS = 30.0 # 录制帧率
def get_screen_resolution():
"""获取设备屏幕分辨率"""
try:
output = subprocess.check_output(f"adb -s {DEVICE_ID if DEVICE_ID else ''} shell wm size", shell=True)
resolution_str = output.decode('utf-8').split(':')[-1].strip()
width, height = map(int, resolution_str.split('x'))
return width, height
except Exception as e:
print(f"获取分辨率失败: {e}")
return 1080, 1920 # 默认值
def start_minicap_service(width, height):
"""在设备上启动 minicap 服务"""
# -P: 指定输出格式 (w h r p b)
# w: width, h: height, r: rotation, p: pixel format, b: bit rate
# 这里我们使用最简单的 raw 格式
cmd = f"adb -s {DEVICE_ID if DEVICE_ID else ''} shell {MINICAP_PATH} -P {width}x{height}@{width}x{height}/32"
print(f"启动 minicap 服务: {cmd}")
subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def record_screen():
"""主录制函数"""
width, height = get_screen_resolution()
start_minicap_service(width, height)
# 给 minicap 服务一些时间启动
time.sleep(2)
# 连接到 minicap
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# minicap 默认连接端口是 13131
s.connect(('127.0.0.1', 13131))
print("成功连接到 minicap")
# 设置视频编码器
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(OUTPUT_VIDEO, fourcc, FPS, (width, height))
print(f"开始录制,分辨率: {width}x{height},帧率: {FPS}")
start_time = time.time()
while True:
# minicap 协议头部信息 (简化处理,实际更复杂)
# 这里我们假设每次读取一个完整的帧
# 实际 minicap 流有更复杂的头部,可能需要使用专门的库如 frida-minicap
# 此处为简化版,直接读取原始像素数据,可能会不稳定
# 更健壮的方式是使用 frida-minicap 或解析 minicap 协议头
# 这里为了演示,我们假设直接读取像素数据
# 注意:这种方法在大多数设备上可能不工作,因为 minicap 有自己的协议头
# 一个更简单但可能有效的替代方案是使用 adb shell screencap
# 但那样效率很低,不适合录制视频流
# --- 重要提示 ---
# 直接解析 minicap 原始流非常复杂,通常建议使用封装好的库,
# https://github.com/openatx/uiautomator2 (它内部封装了 minicap)
# 或者使用 Frida 动态加载 minicap。
# 以下代码是一个概念性演示,可能无法直接运行。
# 由于直接解析原始流非常复杂,这里我们换一个更实用的方法:使用 adb screencap 循环截图
# 虽然性能不如 minicap,但更简单通用
# 使用 adb screencap 录制 (更简单,但性能较低)
while True:
subprocess.run(f"adb -s {DEVICE_ID if DEVICE_ID else ''} shell screencap -p /sdcard/screen.png", shell=True, check=True)
img = cv2.imread("/sdcard/screen.png")
if img is not None:
out.write(img)
time.sleep(1/FPS)
except ConnectionRefusedError:
print("连接 minicap 失败,请确保服务已启动。")
except Exception as e:
print(f"录制过程中发生错误: {e}")
finally:
if 'out' in locals():
out.release()
s.close()
print(f"录制结束,视频已保存为 {OUTPUT_VIDEO}")
if __name__ == "__main__":
record_screen()
minicap流解析的说明: 直接解析minicap的原始 TCP 流协议相当复杂,因为它包含一个二进制头信息,上面的s.connect(...)部分只是一个概念,实际应用中你需要自己解析这个协议头,或者使用现成的库,对于快速实现,使用adb shell screencap循环截图 是一个更简单(尽管性能较低)的替代方案。
脚本 2: touch_replayer.py (回放)
这个脚本会读取一个包含触摸事件轨迹的文件(通常是 JSON 格式),然后通过 minitouch 将这些事件发送到设备上。
首先创建一个触摸轨迹文件 touch_actions.json
这个文件记录了触摸事件的类型、坐标和时间戳。
[
{
"action": "down",
"x": 500,
"y": 1000,
"timestamp": 0
},
{
"action": "move",
"x": 550,
"y": 1050,
"timestamp": 100
},
{
"action": "move",
"x": 600,
"y": 1100,
"timestamp": 200
},
{
"action": "up",
"x": 600,
"y": 1100,
"timestamp": 300
}
]
touch_replayer.py 脚本
import subprocess
import socket
import time
import json
import os
# --- 配置 ---
DEVICE_ID = "your_device_id"
MINITOUCH_PATH = "/data/local/tmp/minitouch"
TOUCH_ACTIONS_FILE = "touch_actions.json"
def start_minitouch_service():
"""在设备上启动 minitouch 服务"""
# minitouch 默认端口为 1111
cmd = f"adb -s {DEVICE_ID if DEVICE_ID else ''} forward tcp:1111 localabstract:minitouch"
subprocess.run(cmd, shell=True, check=True)
cmd = f"adb -s {DEVICE_ID if DEVICE_ID else ''} shell {MINITOUCH_PATH}"
print(f"启动 minitouch 服务: {cmd}")
# 使用 Popen 保持服务运行
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
time.sleep(1) # 等待服务启动
return process
def replay_touch_actions(actions):
"""回放触摸动作"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 1111))
print("成功连接到 minitouch")
# minitouch 命令格式:
# d <x> <y> <pressure> <contact_id> // 按下或移动
# u <contact_id> // 抬起
# c // 等待操作完成 (commit)
# r // 重置设备状态
s.send(b'r\n') # 发送重置命令,清除之前的状态
start_time = time.time()
for action in actions:
# 计算需要等待的时间
current_time = time.time() - start_time
if action['timestamp'] > current_time:
time.sleep(action['timestamp'] - current_time)
command = ""
if action['action'] == 'down':
command = f"d {action['x']} {action['y']} 50 1\n"
elif action['action'] == 'move':
command = f"d {action['x']} {action['y']} 50 1\n"
elif action['action'] == 'up':
command = f"u 1\n"
if command:
print(f"发送命令: {command.strip()}")
s.send(command.encode('utf-8'))
s.send(b'c\n') # 发送 commit,让命令立即执行
print("所有触摸动作回放完毕。")
except Exception as e:
print(f"回放过程中发生错误: {e}")
finally:
s.close()
# 发送最终重置命令
try:
final_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
final_s.connect(('127.0.0.1', 1111))
final_s.send(b'r\n')
final_s.close()
except:
pass
if __name__ == "__main__":
if not os.path.exists(TOUCH_ACTIONS_FILE):
print(f"错误: 找不到触摸动作文件 {TOUCH_ACTIONS_FILE}")
else:
with open(TOUCH_ACTIONS_FILE, 'r') as f:
actions = json.load(f)
minitouch_process = start_minitouch_service()
try:
replay_touch_actions(actions)
finally:
minitouch_process.terminate() # 结束 minitouch 服务
minitouch_process.wait()
print("minitouch 服务已停止。")
第三步:完整工作流程
- 准备环境: 按照第一步,确保 ADB、
minicap/minitouch服务端和 Python 依赖都已就绪。 - 录制屏幕轨迹:
- 运行
python screen_recorder.py,它会将你的操作录制成一个 MP4 文件。 - 重要: 在录制的同时,你需要手动记录下你操作的坐标和时间,你可以通过
adb shell getevent或一些 UI 测试工具(如 uiautomator2)来辅助获取坐标,或者,你可以开发一个更高级的录制器,它不仅能录视频,还能同时记录minitouch事件。
- 运行
- 生成触摸文件: 根据你记录的坐标和时间,创建
touch_actions.json文件。 - 回放操作:
- 确保你的设备处于你想要回放操作起始状态。
- 运行
python touch_replayer.py。 - 脚本会连接到
minitouch,并根据touch_actions.json文件中的指令,精确地在设备上重现你的触摸操作。
总结与高级方案
- 优点:
- 精确控制:
minicap+minitouch提供了像素级的屏幕控制和事件注入,精度非常高。 - 灵活性: 可以实现复杂的、基于屏幕内容变化的自动化流程。
- 精确控制:
- 缺点:
- 配置复杂: 需要手动下载和部署服务端程序。
- 开发门槛高: 直接解析
minicap流或手动管理触摸事件比较繁琐。
更高级的推荐方案:使用 uiautomator2
对于大多数自动化测试需求,强烈推荐使用 uiautomator2 库,它是一个封装了 ADB、minicap、minitouch 和更多功能的强大 Python 库。
使用 uiautomator2 实现录制回放:
pip install --pre uiautomator2
import uiautomator2 as u2
import time
import json
# 连接设备
d = u2.connect() # 或者 d = u2.connect("你的设备id")
# --- 录制概念 ---
# uiautomator2 本身不直接提供录制功能,但你可以结合它强大的元素定位能力来创建自己的录制器
# 你可以监听点击事件,并获取被点击元素的 bounds,然后将其保存为 JSON
# --- 回放示例 ---
# 假设你有一个 JSON 文件,记录了元素选择器或坐标
touch_actions = [
{"selector": {"text": "设置"}, "action": "click"},
{"selector": {"resourceId": "com.android.settings:id/checkbox"}, "action": "click"},
{"x": 500, "y": 1000, "action": "click"}
]
for action in touch_actions:
if "selector" in action:
# 通过元素选择器操作
element = d(**action["selector"])
element.click()
print(f"点击元素: {action['selector']}")
elif "x" in action and "y" in action:
# 通过坐标操作
d.click(action["x"], action["y"])
print(f"点击坐标: ({action['x']}, {action['y']})")
time.sleep(1) # 模拟操作间隔
uiautomator2 的优势在于它处理了所有底层细节(如 minicap 的连接、minitouch 的发送),并提供了更高级、更稳定的 API(如通过元素 ID、文本、描述等来定位和操作控件),这使得编写自动化脚本变得非常简单和可靠。
