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界面,如下图所示:
在这个界面中我们可以使用鼠标在投屏界面点击、滑动,控制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是Client的实例方法用于启动投屏控制服务,start可以接收两个参数,一个是threaded,默认为False,为True时表示在子线程中开启投屏控制服务;另一个是daemon_threaded,默认为False,为True时表示给子线程开启进程守护。threaded和daemon_threaded有一个为True时,都会在子线程中开启投屏控制服务,都为False时表示在主线程中开启投屏控制服务。要同时开启多个投屏控制服务时,就需要在子线程中开启投屏控制服务,自己创建子线程也是可以的。
server.start()
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方法是Client的实例方法用于移除监听字典listeners中的某个方法,如果我们想不再显示图像时,可以把显示图像的方法从listeners中移除掉。
def no_display(self):
self.server.remove_listener(scrcpy.EVENT_FRAME, self.on_frame)
stop方法是Client的实例方法用于结束投屏控制服务,在我们关闭投屏UI界面时需要结束掉投屏控制服务,及时释放内存资源。
def closeEvent(self, _):
self.server.stop()
我们已经把Android设备的屏幕投射到了电脑上,现在就需要通过一些控制Android设备的方法来操作Android设备。Client的实例属性中有一个control属性,是通过实例化ControlSender类来得到的,ControlSender类就是专门用来控制Android设备的操作类。
self.control = ControlSender(self)
所以我们想要控制Android就需要通过投屏控制对象的control属性。
@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键,先按下再抬起,完成一次按键。
@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)
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)
@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设备的屏幕上下滚动了。
@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)
@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()
@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()
@inject(const.TYPE_COLLAPSE_PANELS)
def collapse_panels(self) -> bytes:
collapse_panels方法是ControlSender类的实例方法用于收起Android设备的下拉通知栏或菜单栏。
def close_panel(self):
self.server.control.collapse_panelsl()
def get_clipboard(self) -> str:
get_clipboard方法是ControlSender类的实例方法用于获取Android设备粘贴板中的内容。在Android设备上复制的文本,我们可以通过这个方法把文本获取出来。
def get_android_clipboard(self):
return self.server.control.get_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)
@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)
@inject(const.TYPE_ROTATE_DEVICE)
def rotate_device(self) -> bytes:
totate_device方法是ControlSender类的实例方法用于旋转Android设备的屏幕。
def totate_screen(self):
self.server.control.totate_device()
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可以创建多个投屏控制服务,然后把这些服务放到一个列表或字典中(最好是字典),来实现控制设备的切换,达到单独控制某台设备或同时操作多台设备的目的。