媒体投射

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类的实例表示。

当您启动一个新活动时,使用MediaProjectionManager系统服务的getMediaProjection()方法创建一个MediaProjection实例。使用来自createScreenCaptureIntent()方法的意图启动活动,以指定屏幕捕获操作。

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

媒体投影尺寸

媒体投影可以捕获整个设备显示屏或应用窗口,而不管窗口模式如何。

初始尺寸

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

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

为了兼容 API 级别 14 及更低版本,请使用 Jetpack WindowManager 库中的WindowMetricsCalculatorcomputeMaximumWindowMetrics()方法。

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

尺寸变化

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

为了确保媒体投影与任何捕获区域以及跨设备旋转的捕获内容的尺寸完全对齐,请使用onCapturedContentResize()回调来调整捕获的大小。(有关更多信息,请参阅后面的自定义部分)。

自定义

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

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

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

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

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

资源回收

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

当媒体投影终止时,会调用此回调,无论是用户手动停止会话,还是系统由于某种原因停止会话。

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

选择退出

Android 14 或更高版本默认启用应用屏幕共享。每个媒体投影会话都允许用户选择共享应用窗口或整个显示屏。

您的应用可以通过使用MediaProjectionConfig参数(该参数从对createConfigForDefaultDisplay()的调用返回)调用createScreenCaptureIntent(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)QPR1 引入了一个新的状态栏芯片,该芯片较大且醒目,应该可以提醒用户任何正在进行的屏幕投影。用户可以点击该芯片以停止其屏幕被共享、投射或录制。

在 Android 15 QPR1 及更高版本上,当设备屏幕锁定时,屏幕投影会自动停止。

图 2. 屏幕共享、屏幕投射和屏幕录制的状态栏芯片。

其他资源

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