投屏技术原理:屏幕镜像与投射、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了
}
}
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
);
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) {
// 每帧更新时回调
}
});
整体流程梳理
好了,我们把整个投屏流程串起来看看。下面这张图展示了从手机屏幕到车机显示的核心链路:
整个流程其实就三步:授权、创建虚拟显示器、读取画面。但每一步都有很多细节需要注意。比如授权时机的选择,我建议在用户点击"开始投屏"按钮时才请求授权,而不是应用启动时就弹窗。用户还没准备好就被要求授权,体验很不好。
避坑指南
做投屏开发这些年,我踩过的坑不少,挑几个典型的跟大家说说:
- 分辨率适配问题:不同手机屏幕分辨率差异很大,直接使用原始分辨率会导致性能问题。我一般会根据车机屏幕尺寸动态计算一个合适的分辨率,比如1080p的手机可以降采样到720p传输,画质损失不大但带宽节省很多。
- 帧率控制:不是所有场景都需要60fps。导航场景30fps就够,视频场景才需要60fps。我曾经在导航场景用了60fps,结果车机发热严重,后来改成动态帧率控制才解决。
- 内存管理:VirtualDisplay和MediaProjection都是重量级对象,一定要在合适的时机释放。我习惯在onPause或onStop中释放,在onResume中重新创建。
- 权限处理:Android 10以后对后台启动Activity有限制,授权Intent的启动方式需要适配。这个坑我是在适配Android 11时发现的,折腾了两天才搞定。
好了,这一章的内容就到这里。投屏技术看起来简单,但真正做好却不容易。下一章我们会深入讲解编码和传输的细节,到时候再跟大家分享更多实战经验。
公众号:蓝海资料掘金营,微信deep3321