媒体投影

借助 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 的大小,以适当的分辨率生成输出。将投射到电视或计算机显示器的界面的尺寸调大(低分辨率),将录像的设备界面尺寸调小(高分辨率)。

从 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 可调整虚拟显示屏和界面的大小,以确保宽高比与捕获的内容相符,并且捕获的内容不会出现信箱模式。

资源恢复

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

当媒体投屏终止时,系统会调用该回调。终止的原因有很多,例如:

  • 用户通过应用的界面或系统的媒体投屏状态栏功能块停止会话
  • 屏幕正在锁定
  • 另一个媒体投屏会话开始
  • 应用进程被终止

如果您的应用未注册回调,则对 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 QPR1 或更高版本的设备上运行的应用,系统会在状态栏中显示一个醒目的大条状标签,以提醒用户正在进行的任何屏幕投影。用户可以点按该条状标签,停止共享、投放或录制其屏幕。此外,当设备屏幕锁定时,屏幕投影会自动停止。

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

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

为确保应用在因用户与状态栏条状标签互动或因锁屏激活而停止屏幕投射时释放资源并更新界面,请执行以下操作:

  • 创建 MediaProjection.Callback 的实例。

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

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

其他资源

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