Android MediaProjection 錄屏方案

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等不規範分辨率,採取策略規避到固定取值範圍。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章