投屏技术原理:屏幕镜像与投射、MediaProjection API、VirtualDisplay创建、Surface与TextureView

各位同学,今天我们来聊聊投屏技术。说实话,车机互联里最核心、最直观的功能就是投屏。你想想看,手机上的导航、音乐、视频,直接投射到车机大屏上,这种体验才是用户真正想要的。

我最早接触投屏是在做手机助手的时候,那时候还是用截屏推流的方式,效率低得可怜。后来Android 5.0推出了MediaProjection API,这才算真正打开了投屏的大门。今天我就把这块技术原理掰开揉碎了讲清楚。

屏幕镜像与投射:两个不同的概念

很多人把屏幕镜像和屏幕投射混为一谈,其实它们有本质区别。我刚开始做车机互联时也踩过这个坑,后来才彻底搞明白。

特性 屏幕镜像 屏幕投射
本质 实时复制手机屏幕内容 将特定媒体内容发送到显示设备
交互方式 手机端操作,车机端同步显示 车机端可独立控制播放
典型协议 Miracast、AirPlay镜像 DLNA、Chromecast
延迟要求 低延迟(<100ms) 可接受较高延迟
使用场景 导航、应用操作 视频播放、音乐投送

说白了,屏幕镜像就是把手机屏幕当做一个摄像头,实时采集画面然后传输。而屏幕投射更像是"告诉"车机去播放某个视频流。在车机互联场景下,我们主要做的是屏幕镜像,因为用户需要在车机上操作手机应用。

MediaProjection API:投屏的基石

Android从5.0开始提供了MediaProjection API,这是实现屏幕采集的标准方式。我记得当时看到这个API时,心里那个激动啊——终于不用再依赖root权限了。

使用MediaProjection API需要先获取用户授权。系统会弹出一个确认对话框,用户同意后才能开始采集。这个设计很合理,毕竟屏幕内容涉及隐私。

// 获取MediaProjectionManager
MediaProjectionManager projectionManager = 
    (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);

// 启动授权Intent
Intent intent = projectionManager.createScreenCaptureIntent();
startActivityForResult(intent, REQUEST_CODE_CAPTURE);

// 在onActivityResult中处理授权结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE_CAPTURE && resultCode == RESULT_OK) {
        // 创建MediaProjection实例
        MediaProjection projection = projectionManager.getMediaProjection(resultCode, data);
        // 接下来就可以用这个projection创建VirtualDisplay了
    }
}
小提示:MediaProjection实例是重量级对象,记得在不用的时候及时释放。我见过不少开发者因为忘记释放导致内存泄漏,车机上的内存本来就紧张,这个问题尤其要注意。

VirtualDisplay创建:把屏幕内容"虚拟"出来

有了MediaProjection,下一步就是创建VirtualDisplay。你可以把它理解成一个虚拟的显示器,系统会把屏幕内容渲染到这个虚拟显示器上,然后我们就可以从中读取像素数据了。

创建VirtualDisplay需要指定几个关键参数:宽度、高度、密度和显示标志。这里有个坑——宽高比要和手机屏幕保持一致,否则画面会变形。我曾经在适配不同分辨率手机时吃过这个亏,后来学乖了,动态获取手机屏幕尺寸。

// 获取屏幕尺寸
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int width = metrics.widthPixels;
int height = metrics.heightPixels;
int density = metrics.densityDpi;

// 创建VirtualDisplay
VirtualDisplay virtualDisplay = projection.createVirtualDisplay(
    "ScreenMirrorDisplay",   // 名称,用于调试
    width,                    // 宽度
    height,                   // 高度
    density,                  // 密度
    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, // 标志
    surface,                  // Surface对象,关键!
    null,                     // 回调
    null                      // Handler
);
核心要点:VirtualDisplay的最后一个参数是Surface。这个Surface就是我们要投递画面的地方。你可以把它理解成一个"画布",系统会把屏幕内容画到这块画布上,然后我们从画布上读取数据。

Surface与TextureView:画面的载体

说到Surface,就不得不提TextureView。在Android中,SurfaceView和TextureView都可以提供Surface,但TextureView更灵活——它可以像普通View一样进行变换、动画,而且支持硬件加速。

我个人习惯使用TextureView来做投屏的画面展示。原因很简单:车机上的UI布局经常需要调整,TextureView可以方便地嵌入到各种布局中,还能做缩放、旋转等操作。

// 在布局中添加TextureView
<TextureView
    android:id="@+id/texture_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

// 在代码中获取Surface
TextureView textureView = findViewById(R.id.texture_view);
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
        // SurfaceTexture可用时,创建Surface
        Surface surface = new Surface(surfaceTexture);
        // 将这个surface传给VirtualDisplay
        startScreenCapture(surface);
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
        // 尺寸变化时处理
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
        // 销毁时释放资源
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
        // 每帧更新时回调
    }
});
注意:TextureView的onSurfaceTextureAvailable回调是在UI线程中执行的。如果你在回调中做耗时操作,会导致界面卡顿。我曾经在项目里直接在回调里初始化编码器,结果车机上画面一卡一卡的,排查了半天才发现是这个原因。正确的做法是把耗时操作放到子线程。

整体流程梳理

好了,我们把整个投屏流程串起来看看。下面这张图展示了从手机屏幕到车机显示的核心链路:

投屏技术核心流程 手机屏幕 原始画面数据 MediaProjection 屏幕采集授权 创建虚拟显示器 VirtualDisplay 虚拟显示器 渲染目标:Surface Surface 像素数据载体 可读取/编码 Surface 的两种常见实现方式 TextureView 提供SurfaceTexture 支持变换、动画、硬件加速 MediaCodec 编码为视频流 用于网络传输 关键流程说明 1. 用户授权 → MediaProjection 获取屏幕采集权限 2. 创建 VirtualDisplay 并绑定 Surface → 系统开始渲染画面到 Surface 3. 从 Surface 读取像素数据 → 本地显示(TextureView)或编码传输(MediaCodec)

整个流程其实就三步:授权、创建虚拟显示器、读取画面。但每一步都有很多细节需要注意。比如授权时机的选择,我建议在用户点击"开始投屏"按钮时才请求授权,而不是应用启动时就弹窗。用户还没准备好就被要求授权,体验很不好。

避坑指南

做投屏开发这些年,我踩过的坑不少,挑几个典型的跟大家说说:

  • 分辨率适配问题:不同手机屏幕分辨率差异很大,直接使用原始分辨率会导致性能问题。我一般会根据车机屏幕尺寸动态计算一个合适的分辨率,比如1080p的手机可以降采样到720p传输,画质损失不大但带宽节省很多。
  • 帧率控制:不是所有场景都需要60fps。导航场景30fps就够,视频场景才需要60fps。我曾经在导航场景用了60fps,结果车机发热严重,后来改成动态帧率控制才解决。
  • 内存管理:VirtualDisplay和MediaProjection都是重量级对象,一定要在合适的时机释放。我习惯在onPause或onStop中释放,在onResume中重新创建。
  • 权限处理:Android 10以后对后台启动Activity有限制,授权Intent的启动方式需要适配。这个坑我是在适配Android 11时发现的,折腾了两天才搞定。
核心总结:投屏技术的本质就是"采集→编码→传输→解码→显示"这条链路。MediaProjection负责采集,VirtualDisplay负责中转,Surface负责承载,TextureView负责显示。把这几个组件的关系理清楚,投屏开发就成功了一半。

好了,这一章的内容就到这里。投屏技术看起来简单,但真正做好却不容易。下一章我们会深入讲解编码和传输的细节,到时候再跟大家分享更多实战经验。


公众号:蓝海资料掘金营,微信deep3321