深入浅出,Andorid 端屏幕采集技术实践,附大厂真题面经
用户允许(点击立即开始)后,在 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 配置参考如下:
二、构造 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 回调里,以特定算法有规律地丢弃部分数据,从而降低采集帧率。
六、横竖屏切换
评论