python实现Android实时投屏操控

2024-06-04 9197阅读

scrcpy-client

        python中有一个scrcpy-client库,可以实现Android设备的实时投屏和操控。它和scrcpy实现Android投屏是一样的,都是把一个scrcpy-server.jar文件通过adb推送到Android设备,并利用adb指令执行scrcpy-server.jar开启投屏和操控服务端,电脑端通过python创建客户端来接收视频流数据和发送控制流数据。视频流数据中就是Android实时屏幕数据,控制流数据就是我们在电脑端对Android设备做的操控动作。在scrcpy-client库中作者提供了一个使用PySide6搭建的投屏控制UI界面,可以完成单台Android设备的投屏控制,我们可以自行制作投屏控制界面,完成多台Android设备的投屏控制。

安装指令:pip3 install scrcpy-client

直接使用

        安装好scrcpy-client库后,我们可以通过直接使用作者提供的ui界面来投屏Android设备。

import scrcpy_ui
scrcpy_ui.main()

确保我们的电脑上通过USB连接了一台Android设备,并且Android设备打开了USB调试功能,已允许电脑调式。这时我们就可以通过执行上面的代码,得到Android设备的投屏UI界面,如下图所示:

python实现Android实时投屏操控 第1张

在这个界面中我们可以使用鼠标在投屏界面点击、滑动,控制Android设备的屏幕。可以通过设备序列号下拉框切换设备,Flip勾选后可以得到镜像屏幕。下方的HOME按钮点击后回到主屏幕,相当于按设备的home键。BACK按钮点击后会返回上一个界面,相当于按设备上的back键。还支持键盘输入,我们可以通过电脑的键盘让Android产生按键事件。

自定义使用

        如果你觉得作者提供的UI界面不能满足你的需求,我们还可以自定义UI界面来实现更多的操作方式。前提是你要会使用PySide6这种UI框架,你必须知道如何在UI界面中使用动态元素,如何实现鼠标点击、移动事件,鼠标滚轮滚动事件,键盘输入事件。如果你还不会使用UI界面相关的框架,可以先去学习一下。如果你会UI相关的框架,就接着往下看。

创建投屏服务

        使用scrcpy中的Client类建立投屏控制服务,Client类中的实例化方法如下:

class Client:
    def __init__(
        self,
        device: Optional[Union[AdbDevice, str, any]] = None,
        max_width: int = 0,
        bitrate: int = 8000000,
        max_fps: int = 0,
        flip: bool = False,
        block_frame: bool = False,
        stay_awake: bool = False,
        lock_screen_orientation: int = LOCK_SCREEN_ORIENTATION_UNLOCKED,
        connection_timeout: int = 3000,
        encoder_name: Optional[str] = None,
    ):

device:Android设备的设备序列号(使用adb devices指令可以查看到)。

max_width:图像帧的最大宽度,默认使用Android广播信息中的帧宽度。

bitrate:比特率,默认8000000比特。

max_fps:最大帧数,默认不限制帧数。

flip:翻转图像(镜像图像),默认不镜像。

block_frame:返回非空帧,默认不返回,返回非空帧可能会阻塞openCv2的渲染线程。

stay_awake:连接USB时Android设备屏幕保持常亮,默认不保持常亮。

lock_screen_orientation:锁定屏幕方向(禁止自动旋转屏幕),默认不锁定。

connection_timeout:连接投屏控制服务(socket服务)超时时间,设定时间内未能成功连接则初始化失败,默认3000毫秒。

encoder_name:编码器名称,可选OMX.google.h264.encoder、OMX.qcom.video.encoder.avc、 c2.qti.avc.encoder、c2.android.avc.encoder,默认自动选择。

        我们通过实例化Client类来得到一个Android设备的投屏控制服务对象,假如Android设备的序列号为123456789,我们可以通过如下代码创建投屏控制服务实例对象:

import scrcpy
server = scrcpy.Client(device='123456789', bitrate=100000000)

start方法

        start是Client的实例方法用于启动投屏控制服务,start可以接收两个参数,一个是threaded,默认为False,为True时表示在子线程中开启投屏控制服务;另一个是daemon_threaded,默认为False,为True时表示给子线程开启进程守护。threaded和daemon_threaded有一个为True时,都会在子线程中开启投屏控制服务,都为False时表示在主线程中开启投屏控制服务。要同时开启多个投屏控制服务时,就需要在子线程中开启投屏控制服务,自己创建子线程也是可以的。

server.start()

add_listener方法

        add_listener是Client的实例方法用于设置监听字典listeners,listeners字典中有两个元素,第一个元素的键为frame表示图像帧元素,第二个元素为init表示初始化元素。两个元素的值都是一个空列表,用来存放函数的。如果想把Android设备的屏幕图像放到UI界面的某个元素上,就需要在UI框架中写一个能接收图像、显示图像的方法,再把这个方法添加到listeners字典的第一个元素列表中。如果想在建立投屏控制服务时做一些操作,就在UI框架中写一个操作相关的方法,在把这个方法放到listeners字典的第二个元素列表中。

    def on_frame(self, frame):  # 在使用PySide6的UI框架中定义了一个用于显示图像的方法
        app.processEvents()
        if frame is not None:
            ratio = self.max_width / max(self.client.resolution)
            image = QImage(
                frame,
                frame.shape[1],
                frame.shape[0],
                frame.shape[1] * 3,
                QImage.Format_BGR888,
            )
            pix = QPixmap(image)  # 处理图像
            pix.setDevicePixelRatio(1 / ratio)  # 设置图像大小
            self.ui.label.setPixmap(pix)  # 在UI界面显示图像
            self.resize(1, 1)

在初始化UI界面时把显示图像的方法加入到listener字典的第一个元素中(key='frame')。

    def __init__(self):
        super().__init()
        self.server = scrcpy.Client(device='123456789', bitrate=100000000)
        self.server.add_listener(scrcpy.EVENT_FRAME, self.on_frame)

这里只是举例,实际上的方法需要根据你的需求自己写。如果想要把Android设备的图像展示在UI界面的某个元素上,就必须写一个展示图像的方法,再把这个方法添加到listener字典的第一个元素中(key='frame')。

remove_listener方法

        remove_listener方法是Client的实例方法用于移除监听字典listeners中的某个方法,如果我们想不再显示图像时,可以把显示图像的方法从listeners中移除掉。

    def no_display(self):
        self.server.remove_listener(scrcpy.EVENT_FRAME, self.on_frame)

stop方法

        stop方法是Client的实例方法用于结束投屏控制服务,在我们关闭投屏UI界面时需要结束掉投屏控制服务,及时释放内存资源。

    def closeEvent(self, _):
        self.server.stop()

控制方法

        我们已经把Android设备的屏幕投射到了电脑上,现在就需要通过一些控制Android设备的方法来操作Android设备。Client的实例属性中有一个control属性,是通过实例化ControlSender类来得到的,ControlSender类就是专门用来控制Android设备的操作类。

    self.control = ControlSender(self)

所以我们想要控制Android就需要通过投屏控制对象的control属性。

keycode方法

    @inject(const.TYPE_INJECT_KEYCODE)
    def keycode(
        self, keycode: int, action: int = const.ACTION_DOWN, repeat: int = 0
    ) -> bytes:

        keycode方法是ControlSender类的实例方法用于向Android设备发送按键事件。keycode方法可以接收3个参数,第一个参数keycode表示键值(你需要了解adb键值);第二个参数action表示按下还是抬起,默认是按下;第三个参数repeat表示重复操作次数,想重复按几次。

    def click_home(self):
        self.server.control.keycode(scrcpy.KEYCODE_HOME, scrcpy.ACTION_DOWN)
        self.server.control.keycode(scrcpy.KEYCODE_HOME, scrcpy.ACTION_UP)

点击home键,先按下再抬起,完成一次按键。

text方法

    @inject(const.TYPE_INJECT_TEXT)
    def text(self, text: str) -> bytes:

        text方法是ControlSender类的实例方法用于向Android中输入文本,前提是Android设备中的某个输入框被激活了。text方法接收一个参数,就是我们要在Android设备中输入的文本内容。

    def input_text(self, text):
        self.server.control.text(text)

touch方法

    def touch(
        self, x: int, y: int, action: int = const.ACTION_DOWN, touch_id: int = -1
    ) -> bytes:

        touch方法是ControlSender类的实例方法用于Android设备屏幕的多点触控。touch方法可以接收4个参数,前两个参数为触点的x坐标和y坐标;第三个参数action为按下、移动、抬起;第四个参数为触控事件id,默认为-1,你可以设置不同的id来同时执行多个触控事件,达到多点触控的目的。

    def mouse_move(self, evt: QMouseEvent):
        focused_widget = QApplication.focusWidget()
        if focused_widget is not None:
            focused_widget.clearFocus()
        ratio = self.max_width / max(self.one_client.resolution)
        self.server.control.touch(evt.position().x() / ratio, evt.position().y() / ratio, scrcpy.ACTION_MOVE)

scroll方法

    @inject(const.TYPE_INJECT_SCROLL_EVENT)
    def scroll(self, x: int, y: int, h: int, v: int) -> bytes:

        scroll方法是ControlSender类的实例方法用于Android设备屏幕的滚动事件。scroll方法可以接收4个参数,前两个为滚动点的坐标位置;第三个参数为水平滚动距离;第四个参数为垂直滚动距离。

    def on_wheel(self):
        """鼠标滚轮滚动事件"""
        def wheel(evt: QWheelEvent):
            ratio = self.max_width / max(self.one_client.resolution)
            position_x = evt.position().x() / ratio
            position_y = evt.position().y() / ratio
            angle_x = evt.angleDelta().x()
            angle_y = evt.angleDelta().y()
            if angle_y > 0:
                angle_y = 1
            else:
                angle_y = -1
            self.server.control.scroll(position_x, position_y, angle_x, angle_y)
        return wheel

写出这个方法后,我们就可以使用鼠标滚轮来控制Android设备的屏幕上下滚动了。

back_or_turn_screen_on方法

    @inject(const.TYPE_BACK_OR_SCREEN_ON)
    def back_or_turn_screen_on(self, action: int = const.ACTION_DOWN) -> bytes:

        back_or_turn_screen_on方法是ControlSender类的实例方法用于按返回键,并且如果屏幕关闭了还会唤醒屏幕。只接收一个参数action为按下或抬起。

    def click_back(self):
        self.server.control.back_or_turn_screen_on(scrcpy.ACTION_DOWN)
        self.server.control.back_or_turn_screen_on(scrcpy.ACTION_UP)

expand_notification_panel方法

    @inject(const.TYPE_EXPAND_NOTIFICATION_PANEL)
    def expand_notification_panel(self) -> bytes:

        expand_notification_panel方法是ControlSender类的实例方法用于打开Android设备的下拉通知栏。

    def open_notification(self):
        self.server.control.expand_notification_panel()

expand_settings_panel方法

    @inject(const.TYPE_EXPAND_SETTINGS_PANEL)
    def expand_settings_panel(self) -> bytes:

        expand_settings_panel方法是ControlSender类的实例方法用于打开Android设备的下拉菜单栏。

    def open_settings(self):
        self.server.control.expand_settings_panel()

collapse_panels方法

    @inject(const.TYPE_COLLAPSE_PANELS)
    def collapse_panels(self) -> bytes:

        collapse_panels方法是ControlSender类的实例方法用于收起Android设备的下拉通知栏或菜单栏。

    def close_panel(self):
        self.server.control.collapse_panelsl()

get_clipboard方法

    def get_clipboard(self) -> str:

        get_clipboard方法是ControlSender类的实例方法用于获取Android设备粘贴板中的内容。在Android设备上复制的文本,我们可以通过这个方法把文本获取出来。

    def get_android_clipboard(self):
        return self.server.control.get_clipboard()

set_clipboard方法

    @inject(const.TYPE_SET_CLIPBOARD)
    def set_clipboard(self, text: str, paste: bool = False) -> bytes:

        set_clipboard方法是ControlSender类的实例方法用于设置Android设备粘贴板中的内容。set_clipboard方法可以接收两个参数,第一个参数text为要设置到粘贴板中的文本内容;第二个参数paste为粘贴状态,默认为False,当为True时会立即把文本粘贴到输入框中(Android设备的光标在某个输入框中时)。

    def set_android_clipboard(self, text: str, paste=False):
        self.server.control.set_clipboard(text, paste)

set_screen_power_mode方法

    @inject(const.TYPE_SET_SCREEN_POWER_MODE)
    def set_screen_power_mode(self, mode: int = scrcpy.POWER_MODE_NORMAL) -> bytes:

        set_screen_power_mode方法是ControlSender类的实例方法用于Android设备的屏幕电源模式。默认为正常状态表示开启Android设备的屏幕电源,此时Android设备的屏幕为正常状态。还可以设置为关闭状态(scrcpy.POWER_MODE_OFF),此时Android设备的屏幕为关闭状态,但并不是灭屏状态(屏幕电源关了和灭屏是两回事),投屏界面还是能看到屏幕。通过这种方式可以在投屏操控Android设备时减少Android设备的电源消耗。

    def set_screen_power_mode(self, mode=2):
        self.server.control.set_screen_power_mode(mode)

totate_device方法

    @inject(const.TYPE_ROTATE_DEVICE)
    def rotate_device(self) -> bytes:

        totate_device方法是ControlSender类的实例方法用于旋转Android设备的屏幕。

    def totate_screen(self):
        self.server.control.totate_device()

swipe方法

    def swipe(
        self,
        start_x: int,
        start_y: int,
        end_x: int,
        end_y: int,
        move_step_length: int = 5,
        move_steps_delay: float = 0.005,
    ) -> None:

        swipe方法是ControlSender类的实例方法用于滑动Android设备的屏幕。这个方法是对touch方法的封装,相当于一点触控。swipe方法可以接收6个参数,前4个参数为滑动的起始坐标和终止坐标;第5个参数为步长(每次滑动的距离),默认为5个坐标单位;第6个参数为每滑动一步停顿的时间,默认0.005秒。

    def swipe_event(self, start_x: int, start_y: int, end_x: int, end_y: int, step: int, delay: float):
        self.server.control.swipe(start_x, start_y, end_x, end_y, step, delay)

结语

        我们通过在python的UI框架中使用上面这些方法,就能实现Android设备的投屏控制了,这个投屏控制的应用要做成什么样子完全由你自己的需求和审美来决定。如果你想同时操作多台Android可以创建多个投屏控制服务,然后把这些服务放到一个列表或字典中(最好是字典),来实现控制设备的切换,达到单独控制某台设备或同时操作多台设备的目的。

模型(示例)

        我看评论区都想要代码,这里就为大家提供了一个用于参考的模型。可以同时操控多台设备,也可以选择性的操作某台设备。

# -*- coding: utf-8 -*-
import sys
import threading
import scrcpy
from PySide6.QtGui import QMouseEvent, QImage, QPixmap, QKeyEvent
from adbutils import adb
from PySide6.QtCore import *
from PySide6.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QVBoxLayout, \
    QCheckBox, QLabel, QGridLayout, QSpacerItem, QSizePolicy
from numpy import ndarray
# 创建QApplication对象
if not QApplication.instance():
    app = QApplication([])
else:
    app = QApplication.instance()
items = [i.serial for i in adb.device_list()]  # 设备列表
client_dict = {}  # 设备scrcpy客服端字典
# 为所有设备建立scrcpy服务
for i in items:
    client_dict[i] = scrcpy.Client(device=i, bitrate=1000000000)
def thread_ui(func, *args):
    """
    开启一个新线程任务\n
    :param func: 要执行的线程函数;
    :param args: 函数中需要传入的参数 Any
    :return:
    """
    t = threading.Thread(target=func, args=args)  # 定义新线程
    t.setDaemon(True)  # 开启线程守护
    t.start()  # 执行线程
class SignThread(QThread):
    """信号线程"""
    def __new__(cls, parent: QWidget, func, *types: type):
        cls.__update_date = Signal(*types, name=func.__name__)  # 定义信号(*types)一个信号中可以有一个或多个类型的数据(int,str,list,...)
        return super().__new__(cls)  # 使用父类__new__方法创建SignThread实例对象
    def __init__(self, parent: QWidget, func, *types: type):
        """
        信号线程初始化\n
        :param parent: 界面UI控件
        :param func: 信号要绑定的方法
        :param types: 信号类型,可以是一个或多个(type,...)
        """
        super().__init__(parent)  # 初始化父类
        self.__update_date.connect(func)  # 绑定信号与方法
    def send_sign(self, *args):
        """
        使用SignThread发送信号\n
        :param args: 信号的内容
        :return:
        """
        self.__update_date.emit(*args)  # 发送信号元组(type,...)
class MyWindow(QWidget):
    """UI界面"""
    def __init__(self):
        """UI界面初始化"""
        super().__init__()  # 初始化父级
        self.setWindowTitle('多台手机投屏控制示例(python & scrcpy)')  # 设置窗口标题
        self.max_width = 600  # 设置手机投屏宽度
        # 定义元素
        self.check_box = QCheckBox("控制所有设备")  # 定义是否控制所有设备选择框
        self.back_button = QPushButton("BACK")  # 定义返回键
        self.home_button = QPushButton("HOME")  # 定义home键
        self.recent_button = QPushButton("RECENT")  # 定义最近任务键
        self.video = QLabel("设备屏幕信息加载......")  # 定义手机投屏控制标签
        self.video.setStyleSheet("border-width: 3px;border-style: solid;border-color: black;")  # 定义投屏标签样式
        self.video_list = []  # 定义手机投屏标签列表
        for i in items:
            self.video_list.append(QLabel(i))  # 把投屏标签加入列表
        self.main_layout = QHBoxLayout(self)  # 定义主布局容器
        self.frame_layout = QVBoxLayout()  # 定义投屏操控框容器
        self.button_layout = QHBoxLayout()
        self.device_layout = QVBoxLayout()  # 定义投屏容器
        self.list_layout = QGridLayout()  # 定义投屏列表布局容器
        self.spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)  # 弹性空间
        self.device_spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)  # 弹性空间
        self.v_spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)  # 弹性空间
        # 页面布局
        self.main_layout.addLayout(self.frame_layout)
        self.main_layout.addLayout(self.device_layout)
        self.main_layout.addItem(self.v_spacer)
        self.frame_layout.addWidget(self.video)
        self.frame_layout.addLayout(self.button_layout)
        self.frame_layout.addWidget(self.check_box)
        self.frame_layout.addItem(self.spacer)
        self.button_layout.addWidget(self.back_button)
        self.button_layout.addWidget(self.home_button)
        self.button_layout.addWidget(self.recent_button)
        self.device_layout.addLayout(self.list_layout)
        self.device_layout.addItem(self.device_spacer)
        # 交互事件
        self.back_button.clicked.connect(self.click_key(scrcpy.KEYCODE_BACK))
        self.home_button.clicked.connect(self.click_key(scrcpy.KEYCODE_HOME))
        self.recent_button.clicked.connect(self.click_key(scrcpy.KEYCODE_APP_SWITCH))
        self.video.mousePressEvent = self.mouse_event(scrcpy.ACTION_DOWN)
        self.video.mouseMoveEvent = self.mouse_event(scrcpy.ACTION_MOVE)
        self.video.mouseReleaseEvent = self.mouse_event(scrcpy.ACTION_UP)
        self.keyPressEvent = self.on_key_event(scrcpy.ACTION_DOWN)
        self.keyReleaseEvent = self.on_key_event(scrcpy.ACTION_UP)
        # 所有设备屏幕有序排布,最多15台设备,可按需修改
        if len(items) > 0:
            self.now_device = items[0]
            self.now_client = client_dict[items[0]]
            self.now_client.add_listener(scrcpy.EVENT_FRAME, self.main_frame)
            for num in range(len(items)):
                self.video_list[num].setStyleSheet("border-width: 3px;border-style: solid;border-color: black;")
                self.video_list[num].mousePressEvent = self.switch_video(items[num])
                client = client_dict[items[num]]
                client.add_listener(scrcpy.EVENT_FRAME, self.on_frame(num, client))
                if num 

    免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

    目录[+]