【测试人生】安卓游戏投屏/录屏利器——scrcpy

手机投屏/录屏在测试领域的用途有很多,比如:

  • 作为(自动化)测试报告的一部分,记录测试的实时场景
  • 投屏到电脑,用于UI自动化测试
  • 作为日常测试工作使用

当前手机投屏/录屏的解决方案有两个:STF的minicap以及Genymobile的scrcpy。今天则稍微介绍一下scrcpy,能够兼容各类安卓手机,并且在投屏方面,低延迟与高清晰度兼具。

scrcpy,又名screen copy,分为scrcpy-server以及scrcpy-client,server调用安卓内部的接口的获取屏幕信息,然后发送给client,client解码屏幕信息,完成录制/投屏等功能。同时,client也可以接收输入、点击、拖拽信息,通过swipe、input等操作传达给手机,或是发送给scrcpy-server让server进行操作。因此对于手机手残党来说,采用scrcpy投屏日常玩手机是一个不错的选择。

scrcpy的client实现因人而异,主要用于解码视频以及解析用户操作。当前也有许多出色的解决方案,比如:

  • 默认client,采用SDL2制作
  • QtScrcpy,采用Qt制作
  • guiscrcpy,采用PyQt5制作

其中实现相对比较完善的方案是QtScrcpy,除了投屏、手机操作周边比较完善之外,还提供自定义屏幕按键/拖拽配置。如果有日常使用后者是自动化用例录制的需求,对QtScrcpy进行二次开发是很好的选择。

在原理方面,scrcpy调用了MediaCodec接口。编码器会不断从Input Surface中获取屏幕数据,然后进行编码通过socket发送到client中。从官方文档也可以获知,Surface存储了屏幕数据,采用Surface为编码器传递数据会是更加适宜的选择。scrcpy编码屏幕数据的相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
device.setRotationListener(this);
boolean alive;
try {
do {
MediaCodec codec = createCodec();
IBinder display = createDisplay();
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
// include the locked video orientation
Rect videoRect = screenInfo.getVideoSize().toRect();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();

setSize(format, videoRect.width(), videoRect.height());
configure(codec, format);
Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start();
try {
alive = encode(codec, fd);
// do not call stop() on exception, it would trigger an IllegalStateException
codec.stop();
} finally {
destroyDisplay(display);
codec.release();
surface.release();
}
} while (alive);
} finally {
device.setRotationListener(null);
}
}

private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try {
if (consumeRotationChange()) {
// must restart encoding with new size
break;
}
if (outputBufferId >= 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);

if (sendFrameMeta) {
// 将编码数据+长度写到socket,发给scrcpy的client
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
}

IO.writeFully(fd, codecBuffer);
}
} finally {
if (outputBufferId >= 0) {
codec.releaseOutputBuffer(outputBufferId, false);
}
}
}

return !eof;
}

如果只需要单纯实现录屏,采用原生scrcpy-client的no display选项就能够实现。如果用程序控制scrcpy的录屏,建议选择mkv格式录制,并通过adb shell pkill app_process杀死scrcpy-server来达到终止录屏的效果。否则可能造成视频损坏。如果采用系统的screenrecord方案,部分手机可能会不支持,因此可以考虑两者相辅相成。

总之,scrcpy有很多用途值得挖掘,尤其在移动/游戏测试领域,scrcpy未来上应有和minicap相提并论的空间。

最近忙碌,难以抽时间深入研究技术,再加上安卓底层/视频技术方面也是第一次接触,便纯当抛砖引玉。有叙述不妥之处,欢迎指正!

版权声明
本文为博客HiKariのTechLab原创文章,转载请标明出处,谢谢~~~