scrcpy投屏真的很爽
最近在做一个云真机平台,参考sonic的投屏方式, 研究了下
scrcpy
,此处进行记录备忘过程中主要参考的文章:scrcpy官网, python的一个三方库scrcpy-client
为什么会去参考python
的三方库: 因为我的代码水平太烂了, 官方自带的客户端源码我看不懂。
话不多说,进入主题。
先说结论:
通过阅读官方文档可知:
1、scrcpy
主要分为两部分:客户端和服务端, 客户端主要用于展示服务端传递过来的对象画面和音频(音频是最近版本才加的,好像是2.0
之后,只支持Android11
以上系统)
2、scrcpy
服务端其实就是一个jar
包。将jar
包push
到手机设备, 然后启动jar
服务,最后客户端和服务端建立通信, 服务端给客户端返回, 再由客户端将消息内容展示出来。构成了一个完整闭环。
此次主要分析的是
scrcpy
的闭环过程, 期望可以用python
代码实现客户端部分的功能。
先来看下三方库的一段简单代码:
import scrcpy
client = scrcpy.Client()
import cv2
def on_frame(frame):
if frame is not None:
cv2.imshow("viz", frame)
cv2.waitKey(1)
client.add_listener(scrcpy.EVENT_FRAME, on_frame)
client.start(threaded=True)
client.control.touch(100, 200, scrcpy.ACTION_DOWN)
client.control.touch(100, 200, scrcpy.ACTION_UP)
代码不是很复杂,大概是:
1、先使用scrcpy.Client()
创建一个客户端对象
2、然后通过client.add_listener(scrcpy.EVENT_FRAME, on_frame)
添加了一个监听器,监听器拿到每一帧的画面然后使用cv2将画面渲染出来
3、使用client.start
启动客户端
4、使用client.control
进行客户端输入操作(上述代码是按下和弹起的操作)
下面按照上述的顺序进行简单分析:
1、创建客户端对象
追到源码:创建对象的时候可以传这么多参数过去。
init
初始化方法完整代码如上:
device
参数有一小段逻辑:如果没有传参数则使用device_list
列表的第一个对象赋值给device
,否则使用另外的逻辑(传进来的值)去获取设备对象赋值给device
。这里adb
的方法它使用了另外一个三方库:adbutils
if device is None:
device = adb.device_list()[0]
elif isinstance(device, str):
device = adb.device(serial=device)
self.device = device
其他的初始化参数和内容可以暂时不管,后面主逻辑中用到会提。
2、添加监听器
demo
代码如下:
client.add_listener(scrcpy.EVENT_FRAME, on_frame)
方法源码如下:
初始化代码如下:
这部分看起来不难理解:
初始化的时候创建了一个空的字典对象,
add_listener
方法会将接收到的方法给添加进对应对象中。
demo
代码中向scrcpy.EVENT_FRAME
中添加了一个on_frame
方法。scrcpy.EVENT_FRAME
定义如下图所示:
此时打印一下字典对象输出:
3、启动客户端client.start
这里是本文最重点的地方了。
源码如下:
源码主要做了几个事情:
1)__deploy_server()
: 初始化scrcpy
服务
2)__init_server_connection()
:启动scrcpy
服务,建立通信
3)__send_to_listeners(EVENT_INIT)
:监听器管理
4)__stream_loop()
:开始通信
3.1、__deploy_server()
: 初始化scrcpy
服务
源码如下:
这里主要做了两件事情:
a、使用self.device.sync.push
将jar
包push
到了移动设备的/data/local/tmp目录下
, self.device
上面提到过,这里就不再叙述了。sync.push
也是adbutils
三方库的方法
b、使用self.device.shell
。去执行了启动jar
服务的命令。.shell
方法也是adbutils
三方库的方法。
执行的命令如下:
这部分跟scrcpy
官方的说法基本一致
这里有个特别注意的地方:版本号
使用不同版本的jar
包启动的时候需要使用对应的版本号才可以。比如scrcpy
官网当前最新的版本是2.1.1
,三方库使用的版本则是1.20
,这也是我为什么要去读源码而不直接使用三方库的原因。为了防止后续可能出现的兼容性问题,可能需要迭代jar
包版本来解决
3.2、__init_server_connection()
:启动scrcpy
服务,建立通信
源码如下:
这个方法里,使用adbutils
三方库创建了两个socket
连接 self.__video_socket
和self.control_socket
。然后对self.__video_socket
进行了一些初始化操作。
这里可以结合scrcpy
官方文档协助理解。
3.3、__send_to_listeners(EVENT_INIT)
:监听器管理
这里是根据传进来的字典的key
执行key对应的方法list
下的每一个方法。 这里可以不用关注,因为EVENT_INIT
对应的监听器为空。三方库官方给了一个添加init
监听器的示例,只是因为个人感觉没实际使用需求所以略过了。示例如下图所示
3.4、__stream_loop()
:开始通信
这一段开始前有一个判断,如果调用start
的时候有传入参数,则创建一个线程执行,否则在主进程执行。执行方法都是一样的__stream_loop
,逻辑代码如下图所示:
__stream_loop
方法代码如下图所示:
方法一开始使用av.codec.CodecContext.create
创建了一个h264
格式的读取器,这里跟官方文档的介绍基本相符,默认使用h264
格式:
然后是一个死循环,循环条件是判断self.alive
的值是否为False
, 这个应该是停止的时候会设置为False
。
然后使用__video_socket
接收了0x10000
的数据.这里其实我现在也还没搞懂为啥要设置这么多。翻阅官方文档也没有找到答案。另外找到一个使用了scrcpy
的三方库 adbblitz
看了下,他设置的默认值是131072
那我就更疑惑了。
大胆猜测一下:应该是因为不同图片复杂度数据大小不一样,所以采用一个固定的值去取,最多前后两帧数据有问题无法生成有效的图片。
接下来,使用了av.codec.CodecContext.create("h264", "r").parse
对数据进行处理。并将处理后的结果赋值给packets
然后在packets
中进行循环,通过av.codec.CodecContext.create("h264", "r").decode
将数据转换成frames
数据
最后, 通过一个for
循环拿到每一帧的图片数据frame
。
最最后, 使用__send_to_listeners
方法去执行存在字典中的key=frame
的方法,就是执行一开始演示三方库使用的代码里定义的on_frame
方法
再看一眼一开始的演示代码就明白了。
import cv2
def on_frame(frame):
if frame is not None:
cv2.imshow("viz", frame)
cv2.waitKey(1)
client.add_listener(scrcpy.EVENT_FRAME, on_frame)
4、整理代码
至此,整个流程捋了一遍。附上我自己捋出来的代码片段:
import struct
import time
from av.codec import CodecContext
from av.error import InvalidDataError
from adbutils import adb, Network, AdbError
# __deploy_server()
jarPath = r"C:\Python\Python37\Lib\site-packages\scrcpy\scrcpy-server.jar"
cmd = ['CLASSPATH=/data/local/tmp/scrcpy-server.jar', 'app_process', '/', 'com.genymobile.scrcpy.Server', '1.20', 'info', '0', '8000000', '0', '-1', 'true', '-', 'false', 'true', '0', 'false', 'false', '-', '-', 'false']
jarPath = r"scrcpy/scrcpy-server.jar"
cmd = ['CLASSPATH=/data/local/tmp/scrcpy-server.jar', 'app_process', '/', 'com.genymobile.scrcpy.Server', '2.1.1', 'log_level=info', 'tunnel_forward=true', 'video=true', 'audio=false', 'control=true', 'video_bit_rate=8000000', 'send_frame_meta=false']
device = adb.device_list()[0]
print(f'device = device')
device.sync.push(jarPath, '/data/local/tmp/scrcpy-server.jar')
print('scrcpy-server.jar pushed')
serverCon = device.shell(cmd, stream=True)
print(f'serverCon = {serverCon.read(10)}') # 这里返回 b'[server] I' 才表示服务启动成功
# __init_server_connection
videoSocket = None
for _ in range(100):
try:
videoSocket = device.create_connection(Network.LOCAL_ABSTRACT, "scrcpy") # 有可能一次创建不成功
print(f'videoSocket = {videoSocket}')
break
except AdbError:
time.sleep(0.1)
dummy_byte = videoSocket.recv(1)
print(f'dummy_byte = {dummy_byte}')
controlSocket = device.create_connection(Network.LOCAL_ABSTRACT, "scrcpy") # 这里一定要有
print(f'controlSocket = {controlSocket}')
deviceName = videoSocket.recv(64).decode("utf-8").rstrip("\x00")
print(f'deviceName = {deviceName}')
res = videoSocket.recv(4)
print(f'res = {res}')
resolution = struct.unpack(">HH", res)
print(f'resolution = {resolution}')
videoSocket.setblocking(False)
# __send_to_listeners 什么都没做
import cv2
def on_frame(frame):
if frame is not None:
cv2.imshow("viz", frame)
cv2.waitKey(1)
# __send_to_listeners('__send_to_listeners')
#__stream_loop
codec = CodecContext.create("h264", "r")
print(f'codec = {codec}')
lastFrame = None
while True:
try:
# raw_h264 = videoSocket.recv(0x10000) # 131072
raw_h264 = videoSocket.recv(131072)
# print(f'raw_h264 = {len(raw_h264)}')
print(f'raw_h264 = {len(raw_h264)}')
packets = codec.parse(raw_h264)
print(f'packets = {len(packets)}')
for packet in packets:
frames = codec.decode(packet)
print(f'frames = {len(frames)}')
for frame in frames:
frame = frame.to_ndarray(format="bgr24")
lastFrame = frame
resolution = (frame.shape[1], frame.shape[0])
on_frame(frame)
except (BlockingIOError, InvalidDataError):
time.sleep(0.01)
pass
# on_frame(None)
上述代码__deploy_server
中开头定义的jar
和cmd
:上面的是使用三方库自带的jar
执行的cmd
, 下面的是使用scrcpy
官方最新版本2.1.1
执行的jar
和cmd
测试下效果:
经过跟北京时间对比。看起来没什么毛病。
4.1 拓展
可以利用上述的代码进行android的截图保存, 实际测试下来, 使用上述方式截图的效率要远高于使用adb进行截图.
5、篇外-control
的使用
最后的最后,看下control
三方库类支持的控制方法有哪些,因为主要是记录投屏分析的,所以这里就不展开来说了。