Android端屏幕采集原理

手机端实时屏幕共享在视频会议、手游直播等场景下有广泛应用。屏幕采集则是整个实时屏幕共享流程的第一步,下面简单介绍下Andorid端屏幕采集的原理。

背景

Android从 4.0 开始就提供了手机录屏方法,但是需要 root 权限。从 5.0 开始,Google开放了系统录屏API:MediaProjectionMediaProjectionManager,不需要root权限,但是会弹出录屏权限申请框,用户同意后才能开始录屏,类似Android6.0之后权限申请流程。

鉴于目前市面上5.0以下的Android手机占比很低且屏幕采集需要root权限实现复杂,接下来我们主要介绍Android5.0及以上版本的屏幕采集原理。

试想一下,一套完整的屏幕采集流程应该是怎样的?屏幕数据源(生产者)在缓冲区产生数据,屏幕数据消费者从缓冲区提取数据使用。不同的消费者可以实现不同的功能,比如录屏保存和录屏直播(屏幕共享)。这些关键的角色在Android端又是由谁来扮演呢?

VirtualDisplay
VirtualDisplay是Android上的虚拟显示器。本文里VirtualDisplay的作用就是抓取屏幕上显示的内容,是屏幕数据的生产者。

Surface
在Android的窗口实现里,Surface对应了一块屏幕数据缓冲区,屏幕数据生产者可以在Surface上生产数据,消费者则从Surface中提取数据使用。

屏幕采集流程

介绍完以上关键角色,我们大致可以画出一套屏幕采集流程图:


android 屏幕采集流程.jpg

下面逐步介绍代码实现。

一、获取MediaProjection
首先需要获取MediaProjectionManager服务,然后通过MediaProjectionManager服务,获取一个申请屏幕采集权限的Intent并启动屏幕采集申请权限界面:

mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
Intent intent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(intent, SCREEN_CAPTURE_REQUEST_CODE);

启动的屏幕采集权限申请界面如下:

Pixel上的屏幕采集权限申请弹窗,各厂商提示语略有差异

用户允许(点击立即开始)后,在onActivityResult回调里根据返回的resultCodedata获取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(Android 10)时,系统加强了对屏幕采集的限制,必须先启动相应的前台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,相应地SurfaceMediaRecoder提供:

Surface surface = mediaRecorder.getSurface();

2.如果屏幕采集数据用来屏幕共享(录屏直播),那么消费者可以是类似MediaCodec这样的编码器,相应地SurfaceMediaCodec提供:

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);

这里简单介绍下SurfaceTextureSurfaceTexture可以用来捕获视频流中的图像帧,不同于 SurfaceView 会将图像显示在屏幕上,SurfaceTexture 对图像流的处理并不直接显示,而是转为 GL 外部纹理。当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 */);
}

参数说明文档如下:

参数说明.png

各参数Android官方文档都有较详细的说明,其中flagsurface这里再额外说明下:

  • flagVirtualDisplay的标记位,一般取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时,传入需要的widthheight值即可。

屏幕采集帧率的上限取决以Android设备的屏幕刷新率,下限是0,即丢弃所有返回数据不处理。采集帧率并不是越高越好,够用就行。比如在低端机上,就算以较高帧率采集屏幕数据,但受限于机器编解码能力,实际上屏幕传输的帧率达不到采集帧率,反而会消耗过多系统资源导致发热、卡顿等现象。这时候就需要适当降低采集帧率。还是以第二步中通过SurfaceTexture生成的Surface为例,在onFrameAvailable回调里,以特定算法有规律地丢弃部分数据,从而降低采集帧率。

六、横竖屏切换
横竖屏切换的场景在游戏直播中屡见不鲜。比如王者荣耀的主播切换账号时,需要先kill掉王者荣耀APP退到手机主界面,然后再打开王者荣耀重新登录,经历了从横屏到竖屏再回到横屏的切换。

屏幕采集当然也需要根据不同的横竖屏模式来做动态调整。调整的前提是如何感知到横竖屏模式的变化。

如果是监听手机物理方向上的翻转,使用OrientationEventListener即可。但是针对某些强制横屏的APP,比如王者荣耀,将手机平放在水平桌面上直接打开这些APP,进入APP后的界面是横屏展示的,这时通过OrientationEventListener检测出来的角度变化无法判断APP界面是否横屏展示。

实际上,我们需要感知的是当前屏幕界面横竖屏展示状态而非手机物理上横竖翻转状态。

这时我们就需要根据Displayrotation值来判断界面的横竖屏状态,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_0ROTATION_180代表竖屏的两种状态,ROTATION_90ROTATION_270代表横屏的两种状态。我们只关心是界面否经历了横竖屏状态的切换,至于左横屏还是右横屏,并不影响采集效果。

private boolean checkRotationChange() {
    int currentRotation = display.getRotation();
    boolean rotationChange = false;
    if ((currentRotation + lastRotation) % 2 == 1) {
        rotationChange = true;
    }
    lastRotation = currentRotation;
    return rotationChange;
}

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容