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
時爆出 Tokenisnull
or IllegalStateException
or 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使用的就是 MediaProjection
+ VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
FLAG
Error
對於 VirtualDisplay
, ImageReader
, MediaCodec
而言,在使用完畢後必定要調用其 release
方法將其釋放,以保證後續調用正常。
旋轉屏幕處理
在直播過程當中,可能須要視頻流隨屏幕旋轉而發生方向變化,此時須要重置解碼器,給予解碼器新的寬高來完成需求。
常見Error
-
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等不規範分辨率,採起策略規避到固定取值範圍。