Android MediaProjection 录屏方案

原创
2020/10/09 17:05
阅读数 1.9K

MediaProjection是Android5.0后提出的一套用于录制屏幕的API,无需root权限。与 MediaProjection协同的类有 MediaProjectionManager, MediaCodec等。

获取MediaProjection对象

申请权限

在使用 MediaPeojection相关API时,需要请求系统级录制屏幕权限,申请权限的方法如下:

//通过getSystemService获取MediaProjectionManager对象

mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);

Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent();

startActivityForResult(captureIntent, REQUEST_CODE);

在 onActivityResult方法中处理回调并初始化 MediaProjection对象

MediaProjection mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);

MediaProjectionManager获取过程

通过 context.getSystemService(MEDIA_PROJECTION_SERVICE)获取 MediaProjectionManager的详细流程:

 

 

Context#getSystemSevice

public abstract @Nullable Object getSystemService(@ServiceName @NonNull String name);

ContextImpl#getSystemService

@Override

public Object getSystemService(String name) {

return SystemServiceRegistry.getSystemService(this, name);

}

SystemServiceRegistry#getSystemService

private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =

new HashMap<String, ServiceFetcher<?>>();
/**

* Statically registers a system service with the context.

* This method must be called during static initialization only.

*/

private static <T> void registerService(String serviceName, Class<T> serviceClass,

ServiceFetcher<T> serviceFetcher) {

SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);

SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);

}
registerService(Context.MEDIA_PROJECTION_SERVICE, MediaProjectionManager.class,

new CachedServiceFetcher<MediaProjectionManager>() {

@Override

public MediaProjectionManager createService(ContextImpl ctx) {

return new MediaProjectionManager(ctx);

}});
/**

* Gets a system service from a given context.

*/

public static Object getSystemService(ContextImpl ctx, String name) {

ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);

return fetcher != null ? fetcher.getService(ctx) : null;

}

申权过程

mMediaProjectionManager.createScreenCaptureIntent()最终启动了一个 Activity,该 Activity位于SystemUI [frameworks/base/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java]下,在其内部有如下代码:

onCreate() Method



mPackageName = getCallingPackage();

//从ServiceManager中获取MEDIA_PROJECTION_SERVICE的Binder代理对象

IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);

mService = IMediaProjectionManager.Stub.asInterface(b);

if (mPackageName == null) {

finish();

return;

}

//获取调起页面的ApplicationInfo

PackageManager packageManager = getPackageManager();

ApplicationInfo aInfo;

try {

aInfo = packageManager.getApplicationInfo(mPackageName, 0);

mUid = aInfo.uid;

} catch (PackageManager.NameNotFoundException e) {

Log.e(TAG, "unable to look up package name", e);

finish();

return;

}



try {

//如果该应用已经已经授权则授权成功,其中permanentGrant是和用户是否点击了不再提示关联的

if (mService.hasProjectionPermission(mUid, mPackageName)) {

setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName,false /*permanentGrant*/));

finish();

return;

}

} catch (RemoteException e) {

Log.e(TAG, "Error checking projection permissions", e);

finish();

return;

}
 
//点击立即开始会回调到这个activity

private Intent getMediaProjectionIntent(int uid, String packageName, boolean permanentGrant/*和不再显示关联,true:勾选不再显示,false:未勾选*/)

throws RemoteException {

IMediaProjection projection = mService.createProjection(uid, packageName,

MediaProjectionManager.TYPE_SCREEN_CAPTURE, permanentGrant);

Intent intent = new Intent();

intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());

return intent;

}

录屏悬浮窗

一般对于悬浮窗我们使用 WindowManager.addView(Viewview)的实现方式,常见的 WindowType为 TYPE_SYSTEM_ALERT,这种Type需要申请悬浮窗权限,在manifest里面注册

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW"/>

由于国内rom厂商定制严重,导致该权限的申请适配极为繁琐,这里我使用 TYPE_TOAST作为弹出框类型。

//设置Window Type为TYPE_TOAST

mWindowParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_TOAST);



mWindowParams.format = PixelFormat.RGBA_8888;

mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;

mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;

mWindowParams.gravity = mGravity;



mWindowParams.x = mWindowPositionX == 0 ? mScreenWidth : mWindowPositionX;

mWindowParams.y = mWindowPositionY == 0 ? mScreenHeight : mWindowPositionY;

mWindowManager.addView(mWindowView,mWindowParams);

PhoneWindowManager#checkAddPermission
/** {@inheritDoc} */

@Override

public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {

int type = attrs.type;



outAppOp[0] = AppOpsManager.OP_NONE;



if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)

|| (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)

|| (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {

return WindowManagerGlobal.ADD_INVALID_TYPE;

}



if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {

// Window manager will make sure these are okay.

return ADD_OKAY;

}



//check window type

if (!isSystemAlertWindowType(type)) {

switch (type) {

case TYPE_TOAST:

// Only apps that target older than O SDK can add window without a token, after

// that we require a token so apps cannot add toasts directly as the token is

// added by the notification system.

// Window manager does the checking for this.

outAppOp[0] = OP_TOAST_WINDOW;

return ADD_OKAY;

case TYPE_DREAM:

case TYPE_INPUT_METHOD:

case TYPE_WALLPAPER:

case TYPE_PRESENTATION:

case TYPE_PRIVATE_PRESENTATION:

case TYPE_VOICE_INTERACTION:

case TYPE_ACCESSIBILITY_OVERLAY:

case TYPE_QS_DIALOG:

// The window manager will check these.

return ADD_OKAY;

}

return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)

== PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;

}



// Things get a little more interesting for alert windows...

outAppOp[0] = OP_SYSTEM_ALERT_WINDOW;



final int callingUid = Binder.getCallingUid();

// system processes will be automatically granted privilege to draw

if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {

return ADD_OKAY;

}



ApplicationInfo appInfo;

try {

appInfo = mContext.getPackageManager().getApplicationInfoAsUser(

attrs.packageName,

0 /* flags */,

UserHandle.getUserId(callingUid));

} catch (PackageManager.NameNotFoundException e) {

appInfo = null;

}



if (appInfo == null || (type != TYPE_APPLICATION_OVERLAY && appInfo.targetSdkVersion >= O)) {

/**

* Apps targeting >= {@link Build.VERSION_CODES#O} are required to hold

* {@link android.Manifest.permission#INTERNAL_SYSTEM_WINDOW} (system signature apps)

* permission to add alert windows that aren't

* {@link android.view.WindowManager.LayoutParams#TYPE_APPLICATION_OVERLAY}.

*/

return (mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)

== PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;

}



// check if user has enabled this operation. SecurityException will be thrown if this app

// has not been allowed by the user

final int mode = mAppOpsManager.checkOpNoThrow(outAppOp[0], callingUid, attrs.packageName);

switch (mode) {

case AppOpsManager.MODE_ALLOWED:

case AppOpsManager.MODE_IGNORED:

// although we return ADD_OKAY for MODE_IGNORED, the added window will

// actually be hidden in WindowManagerService

return ADD_OKAY;

case AppOpsManager.MODE_ERRORED:

// Don't crash legacy apps

if (appInfo.targetSdkVersion < M) {

return ADD_OKAY;

}

return ADD_PERMISSION_DENIED;

default:

// in the default mode, we will make a decision here based on

// checkCallingPermission()

return (mContext.checkCallingOrSelfPermission(SYSTEM_ALERT_WINDOW)

== PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;

}

}

录屏

官网对于MediaProjection介绍如下:

A token granting applications the ability to capture screen contents and/or record system audio. The exact capabilities granted depend on the type of MediaProjection.

A screen capture session can be started through createScreenCaptureIntent(). This grants the ability to capture screen contents, but not system audio.

从上述介绍可以看出MediaProjection只是维持一个Token,使得应用具备录屏能力,而正在实现录屏功能则需要配合其他API共同使用。 这时我们就可以引入VirtualDisplay了,VirtualDisplay相当于一个虚拟显示器,会把屏幕上的内容渲染在一个surface上,官网关于VirtualDisplay的介绍如下:

Represents a virtual display. The content of a virtual display is rendered to a Surface that you must provide to createVirtualDisplay().

Because a virtual display renders to a surface provided by the application, it will be released automatically when the process terminates and all remaining windows on it will be forcibly removed. However, you should also explicitly call release() when you're done with it.

注意这里说明了需要主动调用 release()方法释放 VirtualDisplay

Error

在使用 MediaProjection时爆出 Tokenisnullor IllegalStateExceptionor InvalidMediaProjection,此时可以排查当前的 MediaProjection对象,是否在其他地方已经将其release掉了,可以考虑做成全局的MediaProjection,让它的生命周期和Application生命周期同步,以防止token非法问题

创建VirtualDiaplay

/**

*mDisplayWidth,mDisplayHeight指定的是宽高

*mScreenDensity 屏幕密度

*DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR Virtualdisplay的创建flag

*mSurface virtualdisplay渲染的surface

*

**/

Projection.createVirtualDisplay("display

mDisplayWidth, mDisplayHeight, mScreenDensity,

DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,

mSurface, null /*Callbacks*/, null /*Handler*/);

这里重点介绍下 mSurface参数, mSurface参数在 VirtualDisplay初始化完成后,相当于持有了屏幕上的每一帧图像数据,通过操作这个 Surface就可以完成截图或录屏功能[会将屏幕上的内容投影到该Surface上]。

  • 当截图时,我们可以配合 ImageReader使用,传入 ImageReader.getSurface();

  • 当录屏时,我们可以结合 MediaCodec,将该 Surface作为 MediaCodec的输入 Surface使用,传入 MediaCodeC.createInputSurface(),然后按照业务需求进行编解码,选择推流还是录制成文件;

VirtualDiaplay Flags

  • VIRTUAL_DISPLAY_FLAG_PUBLIC:使用该FLAG的VirtualDislay就像HDMI,无线显示之类的链接设备一样,应用程序在设备上的操作内容会被同步镜像显示到该VirtualDiaplay上;

  • VIRTUAL_DISPLAY_FLAG_PRESENTATION:使用该FLAG的VirtualDisplay将被注册成 DISPLAY_CATEGORY_PRESENTATION类别,应用程序可以自动地将其内容投射到显示显示中,以提供更丰富的二次屏幕体验;

  • VIRTUAL_DISPLAY_FLAG_SECURE:使用该FLAG的 VirtualDiaplay,说明在屏幕数据处理过程中,需要防止显示内容被拦截或记录在其他持久化设备上。使用该FLAG需要声明 android.Manifest.permission#CAPTURE_SECURE_VIDEO_OUTPUT权限;

  • VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY:该FLAG与{ VIRTUAL_DISPLAY_FLAG_PUBLIC}一起使用。通常,公共虚拟显示器如果没有自己的窗口,就会自动镜像默认显示的内容。当此标记被指定时,虚拟显示将只显示自己的内容,如果没有窗口,则将被删除。

  • VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR:该FLAG与 VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY互斥,通常与 MediaProjection一起使用,用于创建一个自动同步镜像的虚拟设备

官网的Demo使用的就是 MediaProjectionVIRTUAL_DISPLAY_FLAG_AUTO_MIRROR FLAG

Error

对于 VirtualDisplay, ImageReader, MediaCodec而言,在使用完毕后一定要调用其 release方法将其释放,以保证后续调用正常。

旋转屏幕处理

在直播过程中,可能需要视频流随屏幕旋转而发生方向变化,此时需要重置解码器,给予解码器新的宽高来完成需求。

常见Error

 
  1. Error 1:The producer output buffer format 0x1 does not match the ImageReader's configured buffer format 0x3

  2. Error 2:copyPixelsFromBuffer:Buffer not enough

以上两个错误均是由于初始化ImageReader时传入的Format和创建Bitmap的Format不一致导致的,修改两个Format一样即可

 
  1. invalid MediaProjection

MediaProjection在使用前已经被销毁造成,可以全局保存MediaProjection权限

 
  1. invalid buffer:0Xfffffoe

分辨率错误造成,按照屏幕原始尺寸处理,可能处理12002001,12001848等不规范分辨率,采取策略规避到固定取值范围。

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部