Android 投屏全链路:从应用层到 BSP 的一次彻底拆解
当你把手机画面投到平板上,背后到底发生了什么?这篇从上往下走一遍,覆盖应用 API、framework、AudioFlinger / SurfaceFlinger、HAL,到驱动层。基于 AOSP 标准实现。
一、整体架构:双通道并行的镜像投屏
镜像投屏不是单向"推流",而是手机和接收端之间一直保持双向控制 + 数据通道。手机端做的事:把屏幕画面和音频"复制"一份送进编码器,然后通过网络发到接收端。接收端做的事:解码 + 显示 + 播放。

视频和音频是两条完全独立的通道。这一点很关键------它解释了为什么常见 bug 会出现"画面正常但没声音",反过来基本不存在"有声音但画面卡死"。
二、应用层 API:MediaProjection 体系
Android 应用想要做投屏,标准入口是 MediaProjection(API 21 起)。简化的代码骨架:
java
// 1. 申请投屏权限(弹窗"是否允许录屏")
MediaProjectionManager mpm =
(MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
Intent intent = mpm.createScreenCaptureIntent();
startActivityForResult(intent, REQUEST_CODE);
// 2. 用户同意后拿到 MediaProjection
MediaProjection mp = mpm.getMediaProjection(resultCode, data);
// 3. 创建编码器,拿到 Surface
MediaCodec videoEncoder = MediaCodec.createEncoderByType("video/avc");
videoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = videoEncoder.createInputSurface();
videoEncoder.start();
// 4. 创建虚拟显示器, 输出接到编码器 Surface
VirtualDisplay vd = mp.createVirtualDisplay(
"screen-mirror", // 名字, 后续在 dumpsys 中能看到
width, height, density,
flags,
inputSurface, // ★ 关键: surface 接到编码器
null, null);
// 5. 拉取编码后的 NAL units
while (mirroring) {
int idx = videoEncoder.dequeueOutputBuffer(bufferInfo, timeout);
ByteBuffer nal = videoEncoder.getOutputBuffer(idx);
networkSend(nal);
}
// 6. 音频侧 (Android 10+ 提供应用音频捕获)
AudioPlaybackCaptureConfiguration config =
new AudioPlaybackCaptureConfiguration.Builder(mp)
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
.build();
AudioRecord audioRecord = new AudioRecord.Builder()
.setAudioPlaybackCaptureConfig(config)
.setAudioFormat(...)
.build();
audioRecord.startRecording();
MediaProjection 本质是 framework 给应用的一张"通行证",凭这张证可以创建 VirtualDisplay(视频)和注册 AudioPolicyMix(音频)。
AudioPlaybackCaptureConfiguration 是 Android 10 新增的应用音频捕获 API。它会在 framework 内部翻译为一个 AudioPolicyMix 规则注册到 AudioPolicyManager。
三、视频通道:从 SurfaceFlinger 到编码器
3.1 VirtualDisplay 的本质
VirtualDisplay 不是真显示器,是 SurfaceFlinger 里的一块"虚拟显示层",跟主屏幕一起接收合成结果。

每帧合成时,SurfaceFlinger 会把同一个 layer stack 的内容画到所有目标 display 上。源码位于 frameworks/native/services/surfaceflinger/。
3.2 数据流

Surface 直接接编码器这一点很关键。它不是 CPU 拷贝像素,而是 GPU buffer 直接喂给硬件编码器,零拷贝,所以即使 4K 60fps 也吃得消。
3.3 编码器:MediaCodec 与 Codec2
应用层调的是 MediaCodec,Android 10 起底下走 Codec2 framework:

接收端做的事是反过来:拿 NAL → MediaCodec 解码 → 输出到 Surface(设备屏幕)。
3.4 视频通道为什么很少出问题
视频通道的特点:
- 无状态推流:每一帧画面都是独立的,没有跨帧的"客户端状态"需要和 server 同步
- 断流就是黑帧:编码失败/网络丢包顶多"画面卡几帧",不会出现"画面长时间错乱"
- 驱动稳定:硬件编码器 IP 经过多年迭代,bug 大多被修了
实际中视频通道出问题大都是「黑屏」或「掉帧」,往 MediaCodec stats 和 SurfaceFlinger dumpsys 一看就清楚。
四、音频通道:r_submix loopback 机制
音频投屏比视频复杂。手机的音频体系本来是设计给"播放到喇叭"的,要把它"loopback 出来"喂给镜像编码器,需要走一条特殊的虚拟设备路径。
4.1 整体音频路径

4.2 r_submix(remote_submix)是什么
r_submix 是 AOSP 提供的虚拟音频设备。源码:
hardware/interfaces/audio/aidl/default/r_submix/StreamRemoteSubmix.cpp- 在
audio_policy_configuration.xml里配置成一个 module
它的特殊性:有 sink(写)和 source(读)两端,两端共享一个内存 FIFO。

普通耳机/喇叭的 HAL 只有 sink,PCM 写过去就喂给硬件。r_submix 的 HAL 把 PCM 写到内存 FIFO,让另一个 AudioRecord 从同样的 FIFO 读出来------等价于在内核里把"扬声器"和"麦克风"用一根虚拟线接起来。
4.3 AudioPolicyMix:让 AudioPolicy 知道该把谁的音频送到 r_submix
镜像 App 通过 MediaProjection.createAudioRecord() 实际做的事是:注册一个 AudioPolicyMix 规则到 AudioPolicyManager:
"uid=10295 (player app) 发出的 USAGE_MEDIA 音频,
请帮我 loopback 到 mix `1646668027:ap:41mixp:0`,
我要在那边读"
底层 API:
- Java:
AudioPolicy/AudioMixingRule - Native:
AudioPolicyManager::registerPolicyMixes - 配置可见
frameworks/av/services/audiopolicy/
注册后,AudioPolicyManager 在 getOutputForAttr()(决定一个 AudioTrack 该走哪条 output)时就会把这个 uid 的 MEDIA 音频路由到一个新建的 r_submix output。
4.4 为什么音频通道有"客户端状态"问题
这是和视频最大的区别:音频客户端有持续的状态需要和 server 同步:
- 已写入的 sample 数(
samples_written) - 当前 server 端播放位置(
frame_pos,通过getTimestamp()获取) - 客户端音量(
volume)通过共享内存的cblk字段传给 server mixer - 设备路由(
routedDeviceIds)------ 当前这个 AudioTrack 输出到哪个 device
当路由变化(比如从 SPEAKER 切到 r_submix),底层会触发 restoreTrack_l:销毁旧的 server 端 IAudioTrack,创建新的;客户端还在持有同一个 android::AudioTrack 对象,但底下的 server 端已经换了。
如果客户端对路由变化没有正确响应(不重发音量、不更新状态、不通知 listener),就会出现各种诡异问题------这是音频投屏 bug 的高发区。
五、AudioFlinger / AudioPolicyManager / AudioService 三剑客
Android 音频架构里这三个角色经常被混淆,澄清一下:
| 组件 | 进程 | 职责 |
|---|---|---|
| AudioFlinger | audioserver | 实际混音器。所有 AudioTrack 写来的 PCM,都在这里被混合后送给 HAL |
| AudioPolicyManager | audioserver | 路由决策。决定一个 AudioTrack 该走哪个 output(speaker/HDMI/r_submix),音量怎么算 |
| AudioService | system_server (Java) | 上层管理:音量按键、焦点、播放配置追踪、AudioPlaybackCallback 分发 |
它们的协作:

5.1 PlayerBase:让 AudioService 看见你的 player
任何 AudioTrack(Java 或 native)都应该通过 PlayerBase 注册到 AudioService,否则 AudioService 不知道有这个 player 在播放,相关功能(音量管控、焦点、镜像 captures、电池统计)都会缺失。
Java AudioTrack 默认就调了:
java
// frameworks/base/media/java/android/media/AudioTrack.java
private AudioTrack(...) {
super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK);
...
baseRegisterPlayer(mSessionId);
// 内部 trackPlayer(new PlayerIdCard(...)) 通过 binder 注册
}
native 路径需要主动通过 PlayerBase::init() 注册(frameworks/av/media/libaudioclient/PlayerBase.cpp)。
5.2 路由变化的事件链
这是音频投屏中最关键、也是最容易出 bug 的部分。
!
任何一步出问题,下游都会"看不到 player 状态变化"。
六、HAL 层:硬件抽象的边界
Android Audio HAL 经历了几代演进:
- HAL 1.x:本地共享对象
- HAL 2.x:HIDL
- HAL 3.x(Android 13+):AIDL(当前推荐)
新设备走 AIDL HAL,源码在 hardware/interfaces/audio/aidl/。
6.1 一个 output 的完整通路
!
6.2 音量在哪些层级处理
音量在多层都有处理,理解这个对调音很关键:
| 层级 | 控制范围 | 实现 |
|---|---|---|
| App 端 | 单个 AudioTrack 的 left/right gain | AudioTrack::setVolume(float, float) → 写入 cblk 共享内存 |
| Server cblk | 单个 track 的最终增益 | mixer 读取 cblk 的 mVolumeLR 字段做混音 |
| Stream type | 系统音量 (媒体/通话/铃声) | AudioPolicyManager::checkAndSetVolume,按 stream 全局调 |
| HAL | 硬件音量 (DSP/codec gain) | IDevice::setMasterVolume() 或 effect chain |
| Audio Effect | 各种 effect (EQ/虚拟环绕/降噪) | frameworks/av/media/libeffects |
实际生效的是各层 gain 的乘积。比如 system_volume × app_track_volume × dsp_gain,任何一个为 0 都会导致"无声"。
6.3 r_submix HAL 的特殊性
普通 HAL 实现 IStreamOut::write(buffer, size) 是写到硬件。r_submix HAL 实现的 write 是写到一个内核环形 buffer(pipe),同时另一个 IStreamIn::read 读取这个 buffer。
代码(简化):
cpp
// hardware/interfaces/audio/aidl/default/r_submix/StreamRemoteSubmix.cpp
ndk::ScopedAStatus StreamRemoteSubmix::transfer(...) {
if (mIsInput) {
// 从 sink pipe 读
return mPipeReader->read(buffer, size);
} else {
// 写到 sink pipe
return mPipeWriter->write(buffer, size);
}
}
注意 r_submix HAL 没有自己的音量控制 ------ mixer 写来的 PCM 是什么增益,FIFO 里就是什么增益。这意味着任何"AudioTrack 客户端音量未刷新"的问题会直接表现为"FIFO 里全是静音",比真喇叭路径更容易暴露 bug。
七、BSP / Vendor 层:DSP 与 Codec
到了真硬件这一层,各家 SoC 厂商(高通 / 联发科 / Exynos / 紫光展锐)实现差别就很大了。共通点:

7.1 调试这一层的工具
| 工具 | 用途 |
|---|---|
dumpsys media.audio_flinger |
AudioFlinger 内部状态 |
dumpsys media.audio_policy |
路由决策 / 当前所有 output / mix 列表 |
dumpsys audio |
AudioService 状态 / focus / 全部 player |
tinyplay / tinycap |
直接走 ALSA 测播放/录音 |
cat /proc/asound/cards |
看 ALSA 注册的声卡 |
cat /sys/kernel/debug/asoc/* |
ASoC 路径 / DAPM widget 状态 |
ASoC(ALSA System on Chip)是 ALSA 在嵌入式平台的扩展,连接 codec 驱动和 platform driver。
八、几个常见投屏问题的诊断思路
8.1 接收端"无声音"

定位完通常会发现是某一层"通知没传到"。
8.2 接收端"音画不同步"
视频和音频是两条独立通道,时间戳本来就不严格对齐。常见原因:
- 编码器 latency 不一致(视频比音频晚 50~200ms 是正常的)
- 网络抖动让两条流分开传
- 接收端的渲染线程没用 AVSync
接收端要做的事:用 PTS 对齐 + 一个时钟参考做 drop frame / 静音填充。
8.3 接收端"卡顿"
视频卡:先看 MediaCodec stats(内置的统计:DQin/DQout 比例,编码耗时),再看 dumpsys SurfaceFlinger 的 fps。
音频卡:看 dumpsys media.audio_flinger 的 underrun count。
8.4 切到蓝牙耳机后投屏端无声
通常是音频路由优先级问题。蓝牙优先级高于 r_submix,发生抢路由。解决方法:
- 修改 audio_policy_configuration.xml,调整 priority
- 或镜像 App 在注册 PolicyMix 时设置
MIX_RULE_FLAG_EXCLUSIVE
九、安全与隐私边界
镜像投屏涉及屏幕和音频内容,Android 在多个层级做了限制:
| 层级 | 限制 |
|---|---|
| 应用层 | 必须用 MediaProjection API,每次启动弹窗确认 |
| Window 层 | DRM 内容、银行 App 等带 FLAG_SECURE 的窗口在 VirtualDisplay 上会变黑 |
| AudioPolicy 层 | 只能 capture USAGE_MEDIA / GAME / UNKNOWN 等开放 usage,VOICE_COMMUNICATION 等被禁 |
| 应用清单 | App 自己可以 android:allowAudioPlaybackCapture="false" 拒绝被捕获 |
这些限制不是 vendor 自定义,是 AOSP 标准行为,源码在 services/core/java/com/android/server/audio/ 和 frameworks/native/services/surfaceflinger/。
十、给应用开发者的几条经验
10.1 视频通道
- 编码器优先用 H.265,码率比 H.264 省 30%~50%。但有些老接收端不支持,做协商
- 用 Surface input 方式给编码器喂数据,不要走 ByteBuffer 拷贝
- VirtualDisplay 销毁后再重建会很慢,尽量复用
10.2 音频通道
- AudioPlaybackCaptureConfiguration 的 usage 列表要全:MEDIA / GAME / UNKNOWN / ASSISTANCE_SONIFICATION 至少加上
- 接收端 AudioRecord 的 buffer size 要算好:太小容易 underrun,太大延迟高
- 订阅
AudioManager.registerAudioPlaybackCallback来动态感知 player 变化,比定时 poll 强很多
10.3 控制通道
- TCP(命令) + UDP(数据)的组合比纯 TCP 强很多
- 心跳要做,断网检测要做
- 输入事件反向发送时注意坐标系换算(手机和接收端分辨率/方向可能不同)
一些参考阅读
- AOSP 官方文档:https://source.android.com/docs/core/audio
- AudioPolicy 配置:
frameworks/av/services/audiopolicy/config/audio_policy_configuration.xml - MediaProjection 设计:https://developer.android.com/about/versions/lollipop/android-5.0#ScreenCapture
- AudioPlaybackCapture:https://developer.android.com/guide/topics/media/playback-capture
- HIDL → AIDL HAL 迁移:https://source.android.com/docs/core/audio/aidl
希望这篇能让你对 Android 投屏的「应用 → framework → AudioFlinger/SurfaceFlinger → HAL → BSP」整条链路有个完整的图。下次遇到问题时,知道该往哪一层看。