MediaProjection是Android5.0后提出的一套用于录制屏幕的API,无需root权限。与 MediaProjection
协同的类有 MediaProjectionManager
, MediaCodec
等。
在使用 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);
通过 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> SYSTEM_SERVICE_FETCHERS =
new HashMap>();
/**
* Statically registers a system service with the context.
* This method must be called during static initialization only.
*/
private static void registerService(String serviceName, Class serviceClass,
ServiceFetcher serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
registerService(Context.MEDIA_PROJECTION_SERVICE, MediaProjectionManager.class,
new CachedServiceFetcher() {
@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里面注册
由于国内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
。
在使用 MediaProjection
时爆出 Tokenisnull
or IllegalStateException
or InvalidMediaProjection
,此时可以排查当前的 MediaProjection
对象,是否在其他地方已经将其release掉了,可以考虑做成全局的MediaProjection,让它的生命周期和Application生命周期同步,以防止token非法问题
/**
*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使用的就是 MediaProjection
+ VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
FLAG
对于 VirtualDisplay
, ImageReader
, MediaCodec
而言,在使用完毕后一定要调用其 release
方法将其释放,以保证后续调用正常。
在直播过程中,可能需要视频流随屏幕旋转而发生方向变化,此时需要重置解码器,给予解码器新的宽高来完成需求。
Error 1:The producer output buffer format 0x1 does not match the ImageReader's configured buffer format 0x3
Error 2:copyPixelsFromBuffer:Buffer not enough
以上两个错误均是由于初始化ImageReader时传入的Format和创建Bitmap的Format不一致导致的,修改两个Format一样即可
invalid MediaProjection
MediaProjection在使用前已经被销毁造成,可以全局保存MediaProjection权限
invalid buffer:0Xfffffoe
分辨率错误造成,按照屏幕原始尺寸处理,可能处理12002001,12001848等不规范分辨率,采取策略规避到固定取值范围。