深入浅出,Andorid 端屏幕采集技术实践
背景
屏幕采集流程
一、获取MediaProjection
mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
Intent intent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(intent, SCREEN_CAPTURE_REQUEST_CODE);
用户允许(点击立即开始)后,在 onActivityResult 回调里根据返回的resultCode和 data 获取 MediaProjection:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == SCREEN_CAPTURE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
}
}
java.lang.SecurityException: Media projections require a foreground service
of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
if (REQUIRE_FG_SERVICE_FOR_PROJECTION //1.默认为true
&& requiresForegroundService() //2.当前APP需要启动前台Service
&& !mActivityManagerInternal.hasRunningForegroundService( //3.当前应用没有启动前台service
uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
throw new SecurityException("Media projections require a foreground service"
+ " of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION");
}
//APP TargetSdkVersion大于等于29并且不是特权应用(特权应用一般是系统应用),则返回true(需要启动前台service)
boolean requiresForegroundService () {
return mTargetSdkVersion >= Build.VERSION_CODES.Q && !mIsPrivileged;
}
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!--Service命名自定义,这里仅供参考-->
<service
android:name=".ScreenCapturerService"
android:enabled="true"
android:foregroundServiceType="mediaProjection"/>
Surface surface = mediaRecorder.getSurface();
Surface surface = mediaCodec.createInputSurface();
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface);
Surface surface = surfaceView.getHolder().getSurface();
SurfaceTexture surfaceTexture = new SurfaceTexture(textureId);
surfaceTexture.setOnFrameAvailableListener(new OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
}
}, handler);
Surface surface = new Surface(surfaceTexture);
public VirtualDisplay createVirtualDisplay(String name, int width, int height, int dpi,
int flags, Surface surface, VirtualDisplay.Callback callback, Handler handler) {
DisplayManager dm = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
return dm.createVirtualDisplay(this, name, width, height, dpi, surface, flags, callback,
handler, null /* uniqueId */);
}
参数说明文档如下:
各参数 Android 官方文档都有较详细的说明,其中 flag 和 surface 这里再额外说明下:
flag是VirtualDisplay的标记位,一般取VIRTUAL_DISPLAY_FLAG_PUBLIC即可; surface 也就是上文提到的屏幕数据缓冲区,一般由消费者提供。
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
dealTextureFrame();
}
private void dealTextureFrame() {
...
surfaceTexture.updateTexImage();
float[] transformMatrix = new float[16];
surfaceTexture.getTransformMatrix(transformMatrix);
...
}
屏幕共享(录屏直播)时,高分辨率代表着清晰度,高帧率代表着流畅度。鱼和熊掌,往往不可兼得,尤其是在网络、设备性能受限的情况下。
当手机屏幕在某个界面静止或者界面低速运动时,我们以较低的帧率抓取屏幕即可让接收方观看时不至于产生卡顿掉帧感,这时可以适当提升屏幕采集分辨率,让画质更清晰;相反如果是游戏直播等屏幕界面快速运动等场景,则需要以较高帧率抓取屏幕内容才能让接收方有顺滑观看体验,但在资源受限情况下,可能需要牺牲部分清晰度为代价。
屏幕采集分辨率的控制较为简单,在第三步创建 VirtualDisplay 时,传入需要的 width 和 height 值即可。
屏幕采集帧率的上限取决以 Android 设备的屏幕刷新率,下限是0,即丢弃所有返回数据不处理。采集帧率并不是越高越好,够用就行。比如在低端机上,就算以较高帧率采集屏幕数据,但受限于机器编解码能力,实际上屏幕传输的帧率达不到采集帧率,反而会消耗过多系统资源导致发热、卡顿等现象。这时候就需要适当降低采集帧率。还是以第二步中通过 SurfaceTexture 生成的Surface 为例,在 onFrameAvailable 回调里,以特定算法有规律地丢弃部分数据,从而降低采集帧率。
六、横竖屏切换
横竖屏切换的场景在游戏直播中屡见不鲜。比如王者荣耀的主播切换账号时,需要先kill掉王者荣耀 APP 退到手机主界面,然后再打开王者荣耀重新登录,经历了从横屏到竖屏再回到横屏的切换。
屏幕采集当然也需要根据不同的横竖屏模式来做动态调整。调整的前提是如何感知到横竖屏模式的变化。
如果是监听手机物理方向上的翻转,使用 OrientationEventListener 即可。但是针对某些强制横屏的 APP,比如王者荣耀,将手机平放在水平桌面上直接打开这些 APP,进入 APP 后的界面是横屏展示的,这时通过 OrientationEventListener 检测出来的角度变化无法判断 APP 界面是否横屏展示。
实际上,我们需要感知的是当前屏幕界面横竖屏展示状态而非手机物理上横竖翻转状态。
这时我们就需要根据 Display 的 rotation 值来判断界面的横竖屏状态,rotation 有以下值:
public static final int ROTATION_0 = 0; //默认竖直状态
public static final int ROTATION_90 = 1; //左横屏
public static final int ROTATION_180 = 2; //倒立
public static final int ROTATION_270 = 3; //右横屏
其中ROTATION_0和ROTATION_180代表竖屏的两种状态,ROTATION_90和ROTATION_270代表横屏的两种状态。我们只关心是界面否经历了横竖屏状态的切换,至于左横屏还是右横屏,并不影响采集效果。
private boolean checkRotationChange() {
int currentRotation = display.getRotation();
boolean rotationChange = false;
if ((currentRotation + lastRotation) % 2 == 1) {
rotationChange = true;
}
lastRotation = currentRotation;
return rotationChange;
}
总结
本文针对 Android 端屏幕采集涉及到的屏幕数据生产者,数据缓冲区做了简单介绍,其实消费者对屏幕原始数据的处理更是整个屏幕共享流程中关键的步骤。另外对屏幕采集的分辨率、帧率的控制,横竖屏切换适配等问题也只是理论上阐述,具体代码实现还是有很多细节需要注意。