媒体投影

借助 Android 5(API 级别 21)中引入的 android.media.projection API,您可以将设备屏幕中的内容截取为可播放、录制或投屏到其他设备(如电视)的媒体流。

Android 14(API 级别 34)引入了应用屏幕共享功能,让用户能够分享单个应用窗口(而非整个设备屏幕),无论窗口模式如何。应用屏幕共享功能会将状态栏、导航栏、通知和其他系统界面元素从共享显示屏中排除,即使应用屏幕共享功能用于全屏截取应用也是如此。系统只会分享所选应用的内容。

应用屏幕共享功能可让用户运行多个应用,但仅限于与单个应用共享内容,从而确保用户隐私、提高用户工作效率并增强多任务处理能力。

三种显示表示法

媒体投影会截取设备显示屏或应用窗口中的内容,然后将截取的图像投影到虚拟屏幕上,虚拟屏幕会在 Surface 上呈现该图像。

投影到虚拟屏幕上的真实设备屏幕。写入应用提供的 `Surface` 的虚拟屏幕内容。
图 1. 投影到虚拟屏幕上的真实设备屏幕或应用窗口。写入应用提供的 Surface 的虚拟屏幕。

应用通过 MediaRecorderSurfaceTextureImageReader 提供 Surface,这些接口会使用所截取屏幕的内容,并让您能够实时管理 Surface 上呈现的图像。您可以将图像另存为录像,也可以将其投射到电视或其他设备上。

真实显示

通过获取令牌开始媒体投影会话,该令牌会赋予应用截取设备显示屏或应用窗口中内容的能力。该令牌由 MediaProjection 类的实例表示。

在启动新 activity 时,使用 MediaProjectionManager 系统服务的 getMediaProjection() 方法创建 MediaProjection 实例。使用 createScreenCaptureIntent() 方法中的 intent 启动 activity,以指定屏幕截图操作:

Kotlin

val mediaProjectionManager = getSystemService(MediaProjectionManager::class.java)
var mediaProjection : MediaProjection
val startMediaProjection = registerForActivityResult( StartActivityForResult() ) { result -> if (result.resultCode == RESULT_OK) { mediaProjection = mediaProjectionManager .getMediaProjection(result.resultCode, result.data!!) } }
startMediaProjection.launch(mediaProjectionManager.createScreenCaptureIntent())

Java

final MediaProjectionManager mediaProjectionManager =
    getSystemService(MediaProjectionManager.class);
final MediaProjection[] mediaProjection = new MediaProjection[1];
ActivityResultLauncher startMediaProjection = registerForActivityResult( new StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK) { mediaProjection[0] = mediaProjectionManager .getMediaProjection(result.getResultCode(), result.getData()); } } );
startMediaProjection.launch(mediaProjectionManager.createScreenCaptureIntent());

虚拟屏幕

媒体投影的核心是虚拟屏幕,您可以通过对 MediaProjection 实例调用 createVirtualDisplay() 来创建虚拟屏幕:

Kotlin

virtualDisplay = mediaProjection.createVirtualDisplay(
                     "ScreenCapture",
                     width,
                     height,
                     screenDensity,
                     DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                     surface,
                     null, null)

Java

virtualDisplay = mediaProjection.createVirtualDisplay(
                     "ScreenCapture",
                     width,
                     height,
                     screenDensity,
                     DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                     surface,
                     null, null);

widthheight 参数指定虚拟屏幕的尺寸。如需获取宽度和高度的值,请使用 Android 11(API 级别 30)中引入的 WindowMetrics API。(如需了解详情,请参阅媒体投影大小部分。)

Surface

调整媒体投影 Surface 的大小,以适当的分辨率生成输出。将 Surface 的尺寸调大(低分辨率),以便将屏幕投放到电视或计算机显示器;将 Surface 的尺寸调小(高分辨率),以便录制设备显示屏。

从 Android 12L(API 级别 32)开始,当系统在 Surface 上呈现捕获的内容时,会均匀缩放内容,保持宽高比,以便内容的两个尺寸(宽度和高度)等于或小于 Surface 的相应尺寸。然后,捕获的内容会在 surface 上居中显示。

Android 12L 缩放方法可最大限度地增加 Surface 图像的大小,同时确保适当的宽高比,提升了将屏幕投射到电视和其他大屏幕上的效果。

前台服务权限

如果您的应用以 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

您的应用必须在每次媒体投影会话之前征求用户同意。会话是指对 createVirtualDisplay() 的单次调用。MediaProjection 令牌只能用于一次调用。

在 Android 14 或更高版本中,如果您的应用执行以下任一操作,createVirtualDisplay() 方法都会抛出 SecurityException

  • 将从 createScreenCaptureIntent() 返回的 Intent 实例多次传递给 getMediaProjection()
  • 对同一 MediaProjection 实例多次调用 createVirtualDisplay()

媒体投影大小

无论窗口模式如何,媒体投影都可以截取整个设备显示屏或应用窗口。

初始大小

使用全屏媒体投影时,您的应用必须确定设备屏幕的大小。在应用屏幕共享中,在用户选择截取区域之前,您的应用将无法确定截取显示屏的大小。因此,任何媒体投影的初始大小都是设备屏幕的大小。

使用平台 WindowManager getMaximumWindowMetrics() 方法返回设备屏幕的 WindowMetrics 对象,即使媒体投影宿主应用处于多窗口模式,仅占据部分屏幕也是如此。

如需向下兼容到 API 级别 14,请使用 Jetpack WindowManager 库中的 WindowMetricsCalculator computeMaximumWindowMetrics() 方法。

调用 WindowMetrics getBounds() 方法以获取设备显示屏的宽度和高度。

尺寸变化

当设备旋转或用户在应用屏幕共享中选择应用窗口作为截取区域时,媒体投影的大小可能会发生变化。如果捕获的内容的大小与设置媒体投影时获得的最大窗口指标不同,则媒体投影可能会采用信箱模式。

为确保媒体投影与任何捕获区域的捕获内容大小完全一致,并在设备旋转时保持一致,请使用 onCapturedContentResize() 回调调整捕获内容的大小。(如需了解详情,请参阅下文中的自定义部分)。

自定义

您的应用可以使用以下 MediaProjection.Callback API 自定义媒体投影用户体验:

  • onCapturedContentVisibilityChanged():让托管应用(启动媒体投影的应用)显示或隐藏共享内容。

    您可以使用此回调根据用户是否可以看到所捕获区域来自定义应用的界面。例如,如果您的应用对用户可见,并且在应用界面中显示所截取的内容,并且所截取的应用对用户也是可见的(如通过此回调所指明),则用户会看到相同的内容两次。使用回调更新应用的界面,以隐藏所拍摄的内容并在应用中释放布局空间以供其他内容使用。

  • onCapturedContentResize():使宿主应用能够根据所捕获的显示区域的大小,更改虚拟显示屏上的媒体投影和媒体投影 Surface 的大小。

    每当截取的内容(单个应用窗口或整个设备显示屏)更改大小(由于设备旋转或被截取的应用进入其他窗口模式)时触发。使用此 API 调整虚拟显示屏和 Surface 的大小,以确保宽高比与所捕获的内容相符,并且捕获内容不会出现信箱模式。

资源恢复

您的应用应注册 MediaProjection onStop() 回调,以便在媒体投影会话停止并失效时收到通知。当会话停止时,您的应用应释放其持有的资源,例如虚拟显示屏和投影 surface。已停止的媒体投影会话无法再创建新的虚拟显示屏,即使您的应用之前未为该媒体投影创建虚拟显示屏也是如此。

系统会在媒体投影终止时调用回调。导致这种终止情况的原因有很多,例如:

如果您的应用未注册回调,对 createVirtualDisplay() 的任何调用都会抛出 IllegalStateException

选择停用

Android 14 或更高版本默认启用应用屏幕共享功能。在每次媒体投影会话中,用户可以选择共享应用窗口或整个显示屏。

您的应用可以通过调用 createScreenCaptureIntent(MediaProjectionConfig) 方法并使用从调用 createConfigForDefaultDisplay() 返回的 MediaProjectionConfig 参数来停用应用屏幕共享。

使用从调用 createConfigForUserChoice() 返回的 MediaProjectionConfig 参数调用 createScreenCaptureIntent(MediaProjectionConfig) 与默认行为(即调用 createScreenCaptureIntent())相同。

可调整大小的应用

请始终确保媒体投影应用可调整大小 (resizeableActivity="true")。可调整大小的应用支持设备配置更改和多窗口模式(请参阅多窗口支持)。

如果您的应用不可调整大小,它必须从窗口上下文中查询屏幕边界,并使用 getMaximumWindowMetrics() 检索应用可用的最大屏幕区域的 WindowMetrics

Kotlin

val windowContext = context.createWindowContext(context.display!!,
      WindowManager.LayoutParams.TYPE_APPLICATION, null)
val projectionMetrics = windowContext.getSystemService(WindowManager::class.java)
      .maximumWindowMetrics

Java

Context windowContext = context.createWindowContext(context.getDisplay(),
      WindowManager.LayoutParams.TYPE_APPLICATION, null);
WindowMetrics projectionMetrics = windowContext.getSystemService(WindowManager.class)
      .getMaximumWindowMetrics();

状态栏条状标签和自动停止

屏幕投影漏洞会泄露用户的私密数据(例如财务信息),因为用户不知道自己的设备屏幕正在共享。

Android 15(API 级别 35)及更高版本会显示一个大而醒目的状态栏条状标签,以提醒用户任何正在进行的屏幕投影。用户可以点按该条状标签,停止共享、投放或录制其屏幕。此外,当设备屏幕锁定后,屏幕投影会自动停止。

图 2. 用于屏幕共享、投放和录制的状态栏条状标签。

通过启动屏幕共享、投放或录制来测试媒体投影状态栏条状标签的可用性。条状标签应显示在状态栏中。

为确保当用户与状态栏条状标签互动或通过启用锁定屏幕而停止屏幕投影时,您的应用会释放资源并更新其界面,请执行以下操作:

  • 创建 MediaProjection.Callback 的实例。

  • 实现回调 onStop() 方法。当屏幕投影停止时,系统会调用此方法。释放应用持有的所有资源,并根据需要更新应用界面。

如需测试回调,请点按状态栏条状标签或锁定设备屏幕以停止屏幕投影。验证是否调用了 onStop() 方法,以及应用是否按预期响应。

其他资源

如需详细了解媒体投影,请参阅录制视频和音频播放内容