媒体投影
借助 Android 5(API 级别 21)中引入的 android.media.projection
API,您可以将设备屏幕中的内容截取为可播放、录制或投屏到其他设备(如电视)的媒体流。
Android 14(API 级别 34)引入了应用屏幕共享功能,让用户能够分享单个应用窗口(而非整个设备屏幕),无论窗口模式如何。应用屏幕共享功能会将状态栏、导航栏、通知和其他系统界面元素从共享显示屏中排除,即使应用屏幕共享功能用于全屏截取应用也是如此。系统只会分享所选应用的内容。
应用屏幕共享功能可让用户运行多个应用,但仅限于与单个应用共享内容,从而确保用户隐私、提高用户工作效率并增强多任务处理能力。
权限
如果您的应用以 Android 14 或更高版本为目标平台,则应用清单必须包含 mediaProjection
前台服务类型的权限声明:
<manifest ...><uses-permission android:name="android.permission.FOREGROUND_SERVICE" /><uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /><application ...><serviceandroid:name=".MyMediaProjectionService"android:foregroundServiceType="mediaProjection"android:exported="false"></service></application>
</manifest>
通过调用 startForeground()
启动媒体投影服务。
如果您未在调用中指定前台服务类型,则类型默认为清单中定义的前台服务类型的按位整数。如果清单未指定任何服务类型,系统会抛出 MissingForegroundServiceTypeException
。
获取MediaProjection示例(常规实现)
AndroidManifest.xml
<!-- MediaProjection --><uses-permission android:name="android.permission.FOREGROUND_SERVICE" /><uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /><application><activity android:name=".MediaProjectionTest"/><service android:name=".MediaProjectionService"android:foregroundServiceType="mediaProjection"/></application>
Activity
MediaProjectionManager projMgr;final int REQUEST_CODE = 0x101;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);projMgr = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);startService(new Intent(this, ForgroundMediaProjectionService.class));startActivityForResult(projMgr.createScreenCaptureIntent(), REQUEST_CODE);}@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {if(requestCode == REQUEST_CODE){MediaProjection mp = projMgr.getMediaProjection(resultCode, data);if(mp != null){//mp.stop();//获取到MediaProjection后可以通过MediaCodec编码生成图片/视频/H264流...}}}
Service
@Overridepublic void onCreate() {super.onCreate();}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {Notification notification = null;Intent activity = new Intent(this, MediaProjectionTest.class);activity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {NotificationChannel channel = new NotificationChannel("ScreenRecorder", "Foreground notification",NotificationManager.IMPORTANCE_DEFAULT);NotificationManager manager = getSystemService(NotificationManager.class);manager.createNotificationChannel(channel);notification = new Notification.Builder(this, "ScreenRecorder").setContentTitle("Test").setContentText("Test Screencast...").setContentIntent(PendingIntent.getActivity(this, 0x77,activity, PendingIntent.FLAG_UPDATE_CURRENT)).build();}startForeground(1, notification);return super.onStartCommand(intent, flags, startId);}@Overridepublic IBinder onBind(Intent intent) {return null;}
启动Acrtivity后会弹出授权提示
点击立即开始 Activity.onActivityResult
可以获取到MediaProjection.
如果App是系统应用(android.uid.systtem), 如何跳过授权窗?
- 申请MediaProjection过程拆解
涉及源码
frameworks/base/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
frameworks/base/media/java/android/media/projection/MediaProjectionManager.java
frameworks/base/core/res/res/values/config.xml
frameworks/base/packages/SystemUI/AndroidManifest.xml
frameworks/base/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
函数createScreenCaptureIntent
返回的Intent 指向的是 SystemUI的一个组件:
frameworks/base/media/java/android/media/projection/MediaProjectionManager.java
/*** Returns an Intent that <b>must</b> be passed to startActivityForResult()* in order to start screen capture. The activity will prompt* the user whether to allow screen capture. The result of this* activity should be passed to getMediaProjection.*/public Intent createScreenCaptureIntent() {Intent i = new Intent();final ComponentName mediaProjectionPermissionDialogComponent =ComponentName.unflattenFromString(mContext.getResources().getString(com.android.internal.R.string.config_mediaProjectionPermissionDialogComponent));i.setComponent(mediaProjectionPermissionDialogComponent);return i;}
frameworks/base/core/res/res/values/config.xml
<string name="config_mediaProjectionPermissionDialogComponent" translatable="false">com.android.systemui/com.android.systemui.media.MediaProjectionPermissionActivity</string>
frameworks/base/packages/SystemUI/AndroidManifest.xml
<!-- started from MediaProjectionManager --><activityandroid:name=".media.MediaProjectionPermissionActivity"android:exported="true"android:theme="@style/Theme.SystemUI.MediaProjectionAlertDialog"android:finishOnCloseSystemDialogs="true"android:launchMode="singleTop"android:excludeFromRecents="true"android:visibleToInstantApps="true"/>
MediaProjectionPermissionActivity 就是弹窗的主体
frameworks/base/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@Overridepublic void onCreate(Bundle icicle) {super.onCreate(icicle);mPackageName = getCallingPackage();IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);mService = IMediaProjectionManager.Stub.asInterface(b);if (mPackageName == null) {finish();return;}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 {if (mService.hasProjectionPermission(mUid, mPackageName)) {setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName));finish();return;}} catch (RemoteException e) {Log.e(TAG, "Error checking projection permissions", e);finish();return;}TextPaint paint = new TextPaint();paint.setTextSize(42);CharSequence dialogText = null;CharSequence dialogTitle = null;if (Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName)) {dialogText = getString(R.string.media_projection_dialog_service_text);dialogTitle = getString(R.string.media_projection_dialog_service_title);} else {String label = aInfo.loadLabel(packageManager).toString();// If the label contains new line characters it may push the security// message below the fold of the dialog. Labels shouldn't have new line// characters anyways, so just truncate the message the first time one// is seen.final int labelLength = label.length();int offset = 0;while (offset < labelLength) {final int codePoint = label.codePointAt(offset);final int type = Character.getType(codePoint);if (type == Character.LINE_SEPARATOR|| type == Character.CONTROL|| type == Character.PARAGRAPH_SEPARATOR) {label = label.substring(0, offset) + ELLIPSIS;break;}offset += Character.charCount(codePoint);}if (label.isEmpty()) {label = mPackageName;}String unsanitizedAppName = TextUtils.ellipsize(label,paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString();String appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName);String actionText = getString(R.string.media_projection_dialog_text, appName);SpannableString message = new SpannableString(actionText);int appNameIndex = actionText.indexOf(appName);if (appNameIndex >= 0) {message.setSpan(new StyleSpan(Typeface.BOLD),appNameIndex, appNameIndex + appName.length(), 0);}dialogText = message;dialogTitle = getString(R.string.media_projection_dialog_title, appName);}View dialogTitleView = View.inflate(this, R.layout.media_projection_dialog_title, null);TextView titleText = (TextView) dialogTitleView.findViewById(R.id.dialog_title);titleText.setText(dialogTitle);mDialog = new AlertDialog.Builder(this).setCustomTitle(dialogTitleView).setMessage(dialogText).setPositiveButton(R.string.media_projection_action_text, this).setNegativeButton(android.R.string.cancel, this).setOnCancelListener(this).create();mDialog.create();mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);final Window w = mDialog.getWindow();w.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);mDialog.show();}private Intent getMediaProjectionIntent(int uid, String packageName)throws RemoteException {IMediaProjection projection = mService.createProjection(uid, packageName,MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);Intent intent = new Intent();intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());return intent;}
-
申请成功后返回结果给到申请的Activity:
在getMediaProjectionIntent
函数中, 创建了IMediaProjection
并通过Intent返回给了调用的AppsetResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName));
IMediaProjection projection = mService.createProjection(uid, packageName,MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);Intent intent = new Intent();intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());getMediaProjection(resultCode, data)
-
Activity 调用
getMediaProjection
获取MediaProjection
frameworks/base/media/java/android/media/projection/MediaProjectionManager.java
public MediaProjection getMediaProjection(int resultCode, @NonNull Intent resultData) {if (resultCode != Activity.RESULT_OK || resultData == null) {return null;}IBinder projection = resultData.getIBinderExtra(EXTRA_MEDIA_PROJECTION);if (projection == null) {return null;}return new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection));}
总的来说, 这个流程稍微绕了一点路:
createProjection
的实现
frameworks/base/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@Override // Binder callpublic IMediaProjection createProjection(int uid, String packageName, int type,boolean isPermanentGrant) {if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)!= PackageManager.PERMISSION_GRANTED) {throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to grant "+ "projection permission");}if (packageName == null || packageName.isEmpty()) {throw new IllegalArgumentException("package name must not be empty");}final UserHandle callingUser = Binder.getCallingUserHandle();long callingToken = Binder.clearCallingIdentity();MediaProjection projection;try {ApplicationInfo ai;try {ai = mPackageManager.getApplicationInfoAsUser(packageName, 0, callingUser);} catch (NameNotFoundException e) {throw new IllegalArgumentException("No package matching :" + packageName);}projection = new MediaProjection(type, uid, packageName, ai.targetSdkVersion,ai.isPrivilegedApp());if (isPermanentGrant) {mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA,projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED);}} finally {Binder.restoreCallingIdentity(callingToken);}return projection;}
通过反射, 调用MediaProjectionService的createProjection
注意: 此方法需要有系统权限(android.uid.system)
//android.os.ServiceManager;static Object getService(String name){try {Class ServiceManager = Class.forName("android.os.ServiceManager");Method getService = ServiceManager.getDeclaredMethod("getService", String.class);return getService.invoke(null, name);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}return null;}@SuppressLint("SoonBlockedPrivateApi")static Object asInterface(Object binder){try {Class IMediaProjectionManager_Stub = Class.forName("android.media.projection.IMediaProjectionManager$Stub");Method asInterface = IMediaProjectionManager_Stub.getDeclaredMethod("asInterface", IBinder.class);return asInterface.invoke(null, binder);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}return null;}// private IMediaProjectionManager mService;//android.media.projection.IMediaProjectionManager@SuppressLint("SoonBlockedPrivateApi")public static MediaProjection createProjection(){//Context.java public static final String MEDIA_PROJECTION_SERVICE = "media_projection";//IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);// mService = IMediaProjectionManager.Stub.asInterface(b);IBinder b = (IBinder) getService("media_projection");Object mService = asInterface(b) ;//IMediaProjection projection = mService.createProjection(uid, packageName,//MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);//public static final int TYPE_SCREEN_CAPTURE = 0;try {Logger.i("createProjection", "createProjection");Class IMediaProjectionManager = Class.forName("android.media.projection.IMediaProjectionManager");// public IMediaProjection createProjection(int uid, String packageName, int type, boolean isPermanentGrant)Method createProjection = IMediaProjectionManager.getDeclaredMethod("createProjection", Integer.TYPE, String.class, Integer.TYPE, Boolean.TYPE);Object projection = createProjection.invoke(mService, android.os.Process.myUid(), App.getApp().getPackageName(),0, false);Logger.i("createProjection", "projection created!");//android.media.projection.IMediaProjection;Class IMediaProjection = IInterface.class;//Class.forName("android.media.projection.IMediaProjection");Method asBinder = IMediaProjection.getDeclaredMethod("asBinder");Logger.i("createProjection", "asBinder found");Intent intent = new Intent();// public static final String EXTRA_MEDIA_PROJECTION =// "android.media.projection.extra.EXTRA_MEDIA_PROJECTION";//Bundle extra = new Bundle();//extra.putBinder("android.media.projection.extra.EXTRA_MEDIA_PROJECTION", (IBinder)asBinder.invoke(projection));//intent.putExtra("android.media.projection.extra.EXTRA_MEDIA_PROJECTION", (IBinder)asBinder.invoke(projection));intent.putExtra(Intent.EXTRA_RETURN_RESULT, Activity.RESULT_OK);Object projBinder = asBinder.invoke(projection);Logger.i("createProjection", "asBinder invoke success.");//intent.getExtras().putBinder("android.media.projection.extra.EXTRA_MEDIA_PROJECTION", (IBinder)projBinder);Method putExtra = Intent.class.getDeclaredMethod("putExtra", String.class, IBinder.class);putExtra.invoke(intent, "android.media.projection.extra.EXTRA_MEDIA_PROJECTION", (IBinder)projBinder);Logger.i("createProjection", "putExtra with IBinder success.");MediaProjectionManager projMgr = App.getApp().getMediaProjectionManager();MediaProjection mp = projMgr.getMediaProjection(Activity.RESULT_OK, intent);Logger.i("createProjection", "getMediaProjection " + (mp == null ? " Failed" : "Success"));//new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection));//if(mp != null)mp.stop();return mp;} catch (ClassNotFoundException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}return null;}
参考
Android截屏录屏MediaProjection分享
Android录屏的三种方案
媒体投影
[Android] 使用MediaProjection截屏
android设备间实现无线投屏