深入浅出,Andorid 端屏幕采集技术实践

延寿星君
• 阅读 4474

​前言

随着全球产业链线上化和数字化的加速,移动端实时屏幕共享在各行各业场景下都有了广泛的应用,比如在线教育、视频会议、远程业务咨询、手游直播。而屏幕采集则是实现实时屏幕共享流程中的第一步,本篇技术分享就来跟大家讲讲拍乐云在 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 中提取数据使用。
屏幕采集流程

介绍完以上关键角色,我们大致可以画出一套屏幕采集流程图:
深入浅出,Andorid 端屏幕采集技术实践

下面逐步介绍代码实现。
一、获取MediaProjection

首先需要获取 MediaProjectionManager 服务,然后通过 MediaProjectionManager 服务,获取一个申请屏幕采集权限的 Intent 并启动屏幕采集申请权限界面:
mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
Intent intent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(intent, SCREEN_CAPTURE_REQUEST_CODE);
启动的屏幕采集权限申请界面如下:
深入浅出,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 配置参考如下:
<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 */);

}
参数说明文档如下:

深入浅出,Andorid 端屏幕采集技术实践

各参数 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 端屏幕采集涉及到的屏幕数据生产者,数据缓冲区做了简单介绍,其实消费者对屏幕原始数据的处理更是整个屏幕共享流程中关键的步骤。另外对屏幕采集的分辨率、帧率的控制,横竖屏切换适配等问题也只是理论上阐述,具体代码实现还是有很多细节需要注意。

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
凯特林 凯特林
4年前
移动端H5开发常用技巧总结
html篇常用的meta属性设置meta对于移动端的一些特殊属性,可根据需要自行设置<meta name"screenorientation" content"portrait" //Android 禁止屏幕旋转<meta name"fullscreen" content"yes"             //全屏显示
哪吒 哪吒
4年前
栅格布局xs sm md lg xl
1.单位xl对应屏幕1920px超大显示器lg对应屏幕1200px,小于1920px台式19201080显示器md对应屏幕992px,小于1200px适合笔记本sm对应屏幕768px,小于992px适合平板xs对应屏幕<768手机端屏幕例如有如下代码将屏幕分成3列,进行响应式布局当屏幕的大小变成手机般大小(屏幕宽度<768px)时只显示xs2
Stella981 Stella981
3年前
IBC+Palette 实现屏幕内容编码优化
在屏幕内容编码中采用IBCPalette编码技术可显著提升其压缩效率。本文主要介绍了腾讯屏幕内容编码优化技术探索与实践:加入IBCPalette编码工具集,并针对屏幕内容优化ME模块等。本文由腾讯音视频实验室视频编码技术负责人,王诗涛在LiveVideoStack线上分享中的演讲内容整理而成。文/王诗涛整理/LiveVideo
Wesley13 Wesley13
3年前
IM开发快速入门(二):什么是IM系统的实时性?
本文在编写时参考了博客作者“鹿呦呦”和在线课程“即时消息技术剖析与实战”的相关资料,一并表示感谢。1、引言随着移动互联网络的发展,IM技术的应用已经不仅限于聊天应用本身,它早已融入各种应用形态中,比如:直播中的主播互动、联网游戏中的玩家互动、外卖/打车应用中的实时位置共享、在线教育应用中的互动白板等。在这些风格迥异的应用场景下,IM技术
Wesley13 Wesley13
3年前
2020中国 .NET开发者大会精彩回顾:葡萄城高性能表格技术解读
12月19日,2020中国.NET开发者大会在苏州召开。本次会议以“开源、共享、创新”为主题,结合线下、线上实时同步直播的方式,征集了来自微软、龙芯等知名企业的40余位技术大咖,为50余万名开发者带来了近50场技术讲座和.NET应用实践。葡萄城的表格技术负责人王鸿先生,有幸作为本次大会的演讲嘉宾,向在场的.NET开发者分享了葡萄城高性能表格技术
燕青 燕青
1年前
Dropshare 5 专业的网络文件共享工具 支持M1
是一个Mac应用程序,允许您将文件、屏幕截图和屏幕录制上传到各种托管服务。它提供安全的文件共享功能,并与AmazonS3、GoogleDrive和Dropbox等流行的云存储提供商集成。Dropshare5的一些主要功能包括可自定义的上传目的地、自动屏幕截
布局王 布局王
2个月前
详解鸿蒙Next仓颉开发语言中的全屏模式
大家好,今天跟大家分享一下仓颉开发语言中的全屏模式。和ArkTS一样,仓颉的新建项目默认是非全屏模式的,如果你的应用颜色比较丰富,就会发现屏幕上方和底部的留白,这是应用自动避让了屏幕上方摄像头区域和底部的导航条区域。但是通常我们不需要这些留白,而是希望应用
GeorgeGcs GeorgeGcs
2个月前
【HarmonyOS 5】鸿蒙实现手写板
鸿蒙开发能力HarmonyOSSDK应用服务鸿蒙金融类应用(金融理财一、前言实现一个手写板功能,基本思路如下:创建一个可交互的组件,用户在屏幕上触摸并移动手指时,会根据触摸的位置动态生成路径,并使用黑色描边绘制在屏幕上。当用户按下屏幕时,记录按下点的坐标作