手机投屏/录屏在测试领域的用途有很多,比如:
- 作为(自动化)测试报告的一部分,记录测试的实时场景
- 投屏到电脑,用于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实现因人而异,主要用于解码视频以及解析用户操作。当前也有许多出色的解决方案,比如:
其中实现相对比较完善的方案是QtScrcpy,除了投屏、手机操作周边比较完善之外,还提供自定义屏幕按键/拖拽配置。如果有日常使用后者是自动化用例录制的需求,对QtScrcpy进行二次开发是很好的选择。
在原理方面,scrcpy调用了MediaCodec接口。编码器会不断从Input Surface中获取屏幕数据,然后进行编码通过socket发送到client中。从官方文档也可以获知,Surface存储了屏幕数据,采用Surface为编码器传递数据会是更加适宜的选择。scrcpy编码屏幕数据的相关代码如下:
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相提并论的空间。