共计 5654 个字符,预计需要花费 15 分钟才能阅读完成。
前言
随着寰球产业链线上化和数字化的减速,挪动端实时屏幕共享在各行各业场景下都有了宽泛的利用,比方在线教育、视频会议、近程业务征询、手游直播。而屏幕采集则是实现实时屏幕共享流程中的第一步,本篇技术分享就来跟大家讲讲拍乐云在 Andorid 端屏幕采集的教训实际。
背景
Android 从 4.0 开始就提供了手机录屏办法,然而须要 root 权限。从 5.0 开始,Google 凋谢了零碎录屏 API:MediaProjection 和 MediaProjectionManager,不须要 root 权限,然而会弹出录屏权限申请框,用户批准后能力开始录屏,相似 Android6.0 之后权限申请流程。
鉴于目前市面上 5.0 以下的 Android 手机占比很低且屏幕采集须要 root 权限实现简单,接下来咱们次要介绍 Android5.0 及以上版本的屏幕采集原理。
试想一下,一套残缺的屏幕采集流程应该是怎么的?屏幕数据源(生产者)在缓冲区产生数据,屏幕数据消费者从缓冲区提取数据应用。不同的消费者能够实现不同的性能,比方录屏保留和录屏直播(屏幕共享)。这些要害的角色在 Android 端又是由谁来表演呢?
VirtualDisplayVirtualDisplay 是 Android 上的虚构显示器。本文里 VirtualDisplay 的作用就是抓取屏幕上显示的内容,是屏幕数据的生产者。
Surface 在 Android 的窗口实现里,Surface 对应了一块屏幕数据缓冲区,屏幕数据生产者能够在 Surface 上生产数据,消费者则从 Surface 中提取数据应用。
屏幕采集流程
介绍完以上要害角色,咱们大抵能够画出一套屏幕采集流程图:
上面逐渐介绍代码实现。
一、获取 MediaProjection
首先须要获取 MediaProjectionManager 服务,而后通过 MediaProjectionManager 服务,获取一个申请屏幕采集权限的 Intent 并启动屏幕采集申请权限界面:
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);
}
}
须要特地留神的是,在 targetSdkVersion 大于等于 29 时,零碎增强了对屏幕采集的限度,必须先启动相应的前台 Service,能力失常调用 getMediaProjection 办法,否则会抛异样:
java.lang.SecurityException: Media projections require a foreground service
of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
查看零碎源码发现以下条件语句如果都为 true 则抛出以上异样:
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;
}
前台 Service 配置参考如下:
<uses-permission android:name=”android.permission.FOREGROUND_SERVICE”/>
<!–Service 命名自定义,这里仅供参考 –>
<service
android:name=”.ScreenCapturerService”
android:enabled=”true”
android:foregroundServiceType=”mediaProjection”/>
二、结构 Surface
1. 如果屏幕采集数据用来录制视频,那么消费者能够是 MediaRecoder, 相应地 Surface 由 MediaRecoder 提供:
Surface surface = mediaRecorder.getSurface();
2. 如果屏幕采集数据用来屏幕共享(录屏直播),那么消费者能够是相似 MediaCodec 这样的编码器,相应地 Surface 由 MediaCodec 提供:
Surface surface = mediaCodec.createInputSurface();
3. 如果须要将屏幕采集数据显示在 UI 界面 SurfaceView 上的话,Surface 能够通过以下形式生成:
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface);
Surface surface = surfaceView.getHolder().getSurface();
4. 如果想要更加灵便的掌控整个屏幕采集流程,Surface 还能够通过 SurfaceTexture 生成:
SurfaceTexture surfaceTexture = new SurfaceTexture(textureId);
surfaceTexture.setOnFrameAvailableListener(new OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
}
}, handler);
Surface surface = new Surface(surfaceTexture);
这里简略介绍下 SurfaceTexture。SurfaceTexture 能够用来捕捉视频流中的图像帧,当 SurfaceTexture 中有数据更新时,会触发 onFrameAvailable 回调,此时能够调用 updateTexImage 办法从视频流数据中更新以后数据帧。
三、创立 VirtualDisplay
MediaProjection 有现成的 API 能够调用:
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 也就是上文提到的屏幕数据缓冲区,个别由消费者提供。
四、屏幕采集数据处理
咱们以第二步中通过 SurfaceTexture 生成的 Surface 为例。当 SurfaceTexture 中有数据更新时,会触发 onFrameAvailable 回调,咱们能够在该回调里对数据进行特定的解决。
@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 端屏幕采集波及到的屏幕数据生产者,数据缓冲区做了简略介绍,其实消费者对屏幕原始数据的解决更是整个屏幕共享流程中要害的步骤。另外对屏幕采集的分辨率、帧率的管制,横竖屏切换适配等问题也只是实践上论述,具体代码实现还是有很多细节须要留神。