Android MediaProjection 錄屏方案

MediaProjection是Android5.0後提出的一套用於錄製屏幕的API,無需root權限。與 MediaProjection協同的類有 MediaProjectionManager, MediaCodec等。java

獲取MediaProjection對象

申請權限

在使用 MediaPeojection相關API時,須要請求系統級錄製屏幕權限,申請權限的方法以下:android

//經過getSystemService獲取MediaProjectionManager對象

mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);

Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent();

startActivityForResult(captureIntent, REQUEST_CODE);

在 onActivityResult方法中處理回調並初始化 MediaProjection對象windows

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

MediaProjectionManager獲取過程

經過 context.getSystemService(MEDIA_PROJECTION_SERVICE)獲取 MediaProjectionManager的詳細流程:session

 

 

Context#getSystemSeviceapp

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

ContextImpl#getSystemServiceide

@Override

public Object getSystemService(String name) {

return SystemServiceRegistry.getSystemService(this, name);

}

SystemServiceRegistry#getSystemServicefetch

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]下,在其內部有以下代碼:ui

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裏面註冊this

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

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

因爲國內rom廠商定製嚴重,致使該權限的申請適配極爲繁瑣,這裏我使用 TYPE_TOAST做爲彈出框類型。spa

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

相關文章
相關標籤/搜索