杰瑞科技汇

python minicap 录放

这是一个在自动化测试、UI 测试、机器人流程自动化等领域非常强大的技术组合。

核心概念

  1. Minicap: 一个由 OpenStadio 团队开发的开源项目,它将 Android 设备的屏幕帧实时地以视频流的形式推送到一个 TCP 端口,你的 Python 客户端可以连接到这个端口,接收并解码每一帧图像,从而实现屏幕录制或实时获取屏幕截图。
  2. Minitouch: 与 Minicap 配套,它允许你通过网络向设备发送触摸事件(按下、移动、抬起),这使得你可以将录制的触摸轨迹“回放”到设备上,实现自动化操作。
  3. ADB (Android Debug Bridge): 是我们与 Android 设备通信的基础,我们需要用它来安装 minicapminitouch 的服务端程序,并启动它们。

第一步:环境准备

安装 ADB

确保你的电脑上安装了 ADB 并且可以识别你的 Android 设备。

# 检查设备是否连接
adb devices
# 如果设备未授权,请在设备上弹出授权对话框并确认

准备 Minicap 和 Minitouch 服务端

你需要为你的 Android 设备下载对应架构的 minicapminitouch 可执行文件。

  • 获取设备架构:

    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 脚本:

  1. screen_recorder.py: 用于录制屏幕。
  2. 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 服务已停止。")

第三步:完整工作流程

  1. 准备环境: 按照第一步,确保 ADB、minicap/minitouch 服务端和 Python 依赖都已就绪。
  2. 录制屏幕轨迹:
    • 运行 python screen_recorder.py,它会将你的操作录制成一个 MP4 文件。
    • 重要: 在录制的同时,你需要手动记录下你操作的坐标和时间,你可以通过 adb shell getevent 或一些 UI 测试工具(如 uiautomator2)来辅助获取坐标,或者,你可以开发一个更高级的录制器,它不仅能录视频,还能同时记录 minitouch 事件。
  3. 生成触摸文件: 根据你记录的坐标和时间,创建 touch_actions.json 文件。
  4. 回放操作:
    • 确保你的设备处于你想要回放操作起始状态。
    • 运行 python touch_replayer.py
    • 脚本会连接到 minitouch,并根据 touch_actions.json 文件中的指令,精确地在设备上重现你的触摸操作。

总结与高级方案

  • 优点:
    • 精确控制: minicap + minitouch 提供了像素级的屏幕控制和事件注入,精度非常高。
    • 灵活性: 可以实现复杂的、基于屏幕内容变化的自动化流程。
  • 缺点:
    • 配置复杂: 需要手动下载和部署服务端程序。
    • 开发门槛高: 直接解析 minicap 流或手动管理触摸事件比较繁琐。

更高级的推荐方案:使用 uiautomator2

对于大多数自动化测试需求,强烈推荐使用 uiautomator2 库,它是一个封装了 ADB、minicapminitouch 和更多功能的强大 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、文本、描述等来定位和操作控件),这使得编写自动化脚本变得非常简单和可靠。

分享:
扫描分享到社交APP
上一篇
下一篇