媒体投影

Android 5(API 级别 21)中引入的 android.media.projection API 使您能够将设备显示屏的内容捕获为媒体流,然后播放、录制或投射到其他设备(例如电视)上。

Android 14(API 级别 34)引入了应用屏幕共享,使用户能够共享单个应用窗口,而不是整个设备屏幕,无论处于何种窗口模式。应用屏幕共享会从共享显示屏中排除状态栏、导航栏、通知和其他系统 UI 元素,即使在全屏捕获应用时也是如此。仅共享所选应用的内容。

应用屏幕共享通过让用户能够运行多个应用但仅将内容共享限制在单个应用中,从而确保用户隐私、提高用户工作效率并增强多任务处理能力。

三种显示表示形式

媒体投影捕获设备显示屏或应用窗口的内容,然后将捕获的图像投射到虚拟显示屏,虚拟显示屏在 Surface 上渲染图像。

Real device display projected onto virtual display. Contents of
              virtual display written to application-provided `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 设置为大(低分辨率)用于投射到电视或电脑显示器,设置为小(高分辨率)用于设备显示屏录制。

从 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()

媒体投影尺寸

媒体投影可以捕获整个设备显示屏或应用窗口,无论处于何种窗口模式。

初始尺寸

对于全屏媒体投影,您的应用必须确定设备屏幕的尺寸。在应用屏幕共享中,您的应用直到用户选择了捕获区域后才能确定捕获的显示屏尺寸。因此,任何媒体投影的初始尺寸都是设备屏幕的尺寸。

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

为了兼容到 API 级别 14,请使用 Jetpack WindowManager 库中的 WindowMetricsCalculatorcomputeMaximumWindowMetrics() 方法。

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

尺寸变化

当设备旋转或用户在应用屏幕共享中选择应用窗口作为捕获区域时,媒体投影的尺寸可能会发生变化。如果捕获的内容尺寸与设置媒体投影时获取的最大窗口指标尺寸不同,媒体投影可能会出现黑边(letterboxed)。

为了确保媒体投影对于任何捕获区域和设备旋转都能精确对齐捕获内容的大小,请使用 onCapturedContentResize() 回调来调整捕获大小。(有关详细信息,请参阅后面的定制部分。)

定制

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

  • onCapturedContentVisibilityChanged():使宿主应用(启动媒体投影的应用)能够显示或隐藏共享内容。

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

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

    当捕获的内容(单个应用窗口或整个设备显示屏)尺寸发生变化时触发(由于设备旋转或捕获的应用进入不同的窗口模式)。使用此 API 调整虚拟显示屏和 Surface 的尺寸,以确保宽高比与捕获内容匹配,并且捕获不会出现黑边。

资源回收

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

当媒体投影终止时,系统会调用该回调。终止可能由多种原因引起,例如:

  • 用户使用应用的 UI 或系统的媒体投影状态栏指示停止会话
  • 屏幕被锁定
  • 另一个媒体投影会话开始
  • 应用进程被终止

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

选择退出

Android 14 或更高版本默认启用应用屏幕共享。每个媒体投影会话都为用户提供了共享应用窗口或整个显示屏的选项。

您的应用可以通过调用 createScreenCaptureIntent(MediaProjectionConfig) 方法并传入调用 createConfigForDefaultDisplay() 返回的 MediaProjectionConfig 参数来选择退出应用屏幕共享。

调用 createScreenCaptureIntent(MediaProjectionConfig) 并传入调用 createConfigForUserChoice() 返回的 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 QPR1 或更高版本的设备上运行的应用,状态栏中一个大而显眼的指示会提醒用户正在进行的屏幕投影。用户可以点击该指示来停止屏幕共享、投射或录制。此外,当设备屏幕锁定时,屏幕投影会自动停止。

图 2. 用于屏幕共享、投射和录制的状态栏指示。

通过开始屏幕共享、投射或录制来测试媒体投影状态栏指示是否可用。该指示应出现在状态栏中。

为确保您的应用在屏幕投影因用户与状态栏指示交互或锁屏激活而停止时释放资源并更新其 UI,请执行以下操作:

  • 创建 MediaProjection.Callback 的实例。

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

要测试回调,请点击状态栏指示或锁定设备屏幕以停止屏幕投影。验证 onStop() 方法是否被调用,并且您的应用按预期响应。

更多资源

有关媒体投影的更多信息,请参阅捕获视频和音频播放