媒体投影
借助 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 ...>
<service
android: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;
@Override
protected 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);
}
@Override
protected 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
@Override
public void onCreate() {
super.onCreate();
}
@Override
public 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);
}
@Override
public 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 -->
<activity
android: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
@Override
public 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 call
public 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设备间实现无线投屏