Android投屏MirrorCast全链路

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 statsSurfaceFlinger 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/

注册后,AudioPolicyManagergetOutputForAttr()(决定一个 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 强很多
  • 心跳要做,断网检测要做
  • 输入事件反向发送时注意坐标系换算(手机和接收端分辨率/方向可能不同)

一些参考阅读

希望这篇能让你对 Android 投屏的「应用 → framework → AudioFlinger/SurfaceFlinger → HAL → BSP」整条链路有个完整的图。下次遇到问题时,知道该往哪一层看。

相关推荐
Ehtan_Zheng1 小时前
Kotlin const val vs val:字节码、性能与隐藏陷阱详解
android·kotlin
墨狂之逸才1 小时前
Android TV 垃圾应用清理指南
android
源来猿往2 小时前
记ffmpeg-8.1.1 之Android库编译(window)
android·ffmpeg
恋猫de小郭2 小时前
Android 17 正式版发布,全新 AI 和各种破坏性更新
android·前端·flutter
我命由我123453 小时前
Jetpack Room - Room 查询返回列表无需判空、LIKE 关键字
android·java·开发语言·java-ee·android jetpack·android-studio·android runtime
朝星3 小时前
Android开发[14]:网络优化之OkHttp
android·okhttp·kotlin
私人珍藏库3 小时前
[Android] FX Player-安卓全格式播放器-比MX播放器好用
android·学习·工具·软件·多功能
写点啥呢3 小时前
车机 Android 开机优化复盘:我怎么和 AI 一起把问题定位到 SystemUI
android·人工智能
Peter(阿斯拉)4 小时前
[Android]_[中级]_[如何创建MVVM架构原型]
android·java·架构·mvvm·viewmodel