25、屏幕录制实现:MediaProjection API、虚拟显示、音频内录、权限处理

屏幕录制,说白了就是把手机屏幕上显示的内容抓取下来,存成视频文件。这在直播、录课、游戏录制场景里太常见了。Android 从 5.0 开始提供了 MediaProjection API,算是正式把屏幕录制能力开放给了开发者。

我个人习惯把屏幕录制拆成三个核心环节:权限获取虚拟显示创建音视频编码。今天咱们就一个一个掰开揉碎讲清楚。

核心流程概览

用户授权 → 获取 MediaProjection → 创建 VirtualDisplay → 绑定 Surface → 编码器写入 → 生成 MP4

用户授权弹窗 MediaProjection 实例 创建 VirtualDisplay 绑定 Surface MediaCodec 编码 MP4 音频内录 AudioRecord 音频编码 AAC 音视频混合器 (Muxer)

权限处理:别让用户一脸懵

屏幕录制需要动态申请权限。跟相机、麦克风不一样,它用的是 MediaProjectionManagercreateScreenCaptureIntent() 方法。嗯,这里要注意:这不是普通权限,而是一个 Intent

我记得第一次做屏幕录制时,直接调了 requestPermissions,结果发现根本弹不出授权窗口。后来才明白,屏幕录制走的是 startActivityForResult 路线。

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

// 发起授权请求
Intent intent = projectionManager.createScreenCaptureIntent();
startActivityForResult(intent, REQUEST_CODE_SCREEN_CAPTURE);

// 在 onActivityResult 中处理结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
        if (resultCode == RESULT_OK) {
            // 授权成功,创建 MediaProjection 实例
            mediaProjection = projectionManager.getMediaProjection(resultCode, data);
        } else {
            // 用户拒绝了,给个提示
            Toast.makeText(this, "屏幕录制需要授权才能使用", Toast.LENGTH_LONG).show();
        }
    }
}

避坑指南

我曾经遇到过一个问题:在 Android 10 以上,如果应用没有 FOREGROUND_SERVICE 权限,屏幕录制会在后台被系统杀掉。解决方案是启动一个前台服务,并显示通知。

虚拟显示:把屏幕内容"映射"到 Surface 上

拿到 MediaProjection 之后,下一步就是创建 VirtualDisplay。你可以把它理解成一个"虚拟的屏幕",这个屏幕的内容会直接渲染到你指定的 Surface 上。

说白了,就是系统把真实屏幕的内容,复制了一份到你的 Surface 上。这个 Surface 通常来自 MediaCodec 的输入。

// 创建 VirtualDisplay
virtualDisplay = mediaProjection.createVirtualDisplay(
    "ScreenRecorder",           // 名称,随便起
    width,                      // 屏幕宽度
    height,                     // 屏幕高度
    densityDpi,                 // 屏幕密度
    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,  // 镜像模式
    surface,                    // MediaCodec 提供的 Surface
    null,                       // 回调
    null                        // Handler
);

这里有个细节:VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR 表示虚拟显示的内容和真实屏幕保持一致。如果你不加这个 flag,虚拟显示可能是空白的。我在项目中踩过这个坑,折腾了半天才发现是 flag 没传对。

音频内录:系统声音也能抓

屏幕录制光有画面不行,还得有声音。音频内录分两种情况:

  • 麦克风录音:用 AudioRecord 从麦克风采集,适合录解说
  • 系统内录音:从 AudioRecordREMOTE_SUBMIX 设备采集,适合录游戏声音

我个人建议:如果是录游戏或直播,优先用 REMOTE_SUBMIX。但要注意,这个设备在普通模式下只能录系统声音,麦克风的声音需要另外混音。

// 音频内录配置
AudioRecord audioRecord = new AudioRecord(
    MediaRecorder.AudioSource.REMOTE_SUBMIX,  // 系统内录音源
    sampleRate,           // 采样率,推荐 44100
    AudioFormat.CHANNEL_IN_STEREO,  // 立体声
    AudioFormat.ENCODING_PCM_16BIT, // 16位 PCM
    bufferSize
);

// 读取音频数据
byte[] audioData = new byte[bufferSize];
while (isRecording) {
    int readSize = audioRecord.read(audioData, 0, bufferSize);
    if (readSize > 0) {
        // 将音频数据送入 MediaCodec 编码
        audioEncoder.feedData(audioData, readSize);
    }
}

小技巧

你想想看,如果同时录麦克风和系统声音,就需要做混音处理。我一般用 AudioMixer 或者自己写一个简单的 PCM 叠加算法。注意音量不要溢出,否则会有爆音。

音视频编码与混合

视频用 MediaCodec 编码成 H.264,音频编码成 AAC。最后用 MediaMuxer 把两路流混合成一个 MP4 文件。

// 创建 MediaMuxer
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

// 添加视频轨道
int videoTrackIndex = muxer.addTrack(videoFormat);
// 添加音频轨道
int audioTrackIndex = muxer.addTrack(audioFormat);

// 开始混合
muxer.start();

// 在编码回调中写入数据
// videoBufferInfo 和 audioBufferInfo 分别来自视频和音频编码器
muxer.writeSampleData(videoTrackIndex, videoBuffer, videoBufferInfo);
muxer.writeSampleData(audioTrackIndex, audioBuffer, audioBufferInfo);

// 录制结束
muxer.stop();
muxer.release();

这里有个关键点:时间戳必须同步。视频和音频的 presentationTimeUs 要基于同一个时钟源。我习惯用 System.nanoTime() 来生成时间戳,这样两路流能对齐。

性能优化与注意事项

问题 原因 解决方案
录制卡顿 编码器负载过高 降低分辨率或帧率,使用硬件编码器
音画不同步 时间戳不一致 统一使用 System.nanoTime() 生成时间戳
内存泄漏 VirtualDisplay 未释放 onDestroy 中调用 virtualDisplay.release()
权限被回收 MediaProjection 被系统回收 使用前台服务保持进程活跃

重要提醒

Android 10 以上,屏幕录制必须在前台服务中运行。否则系统会在几分钟后杀掉你的录制进程。我曾经在测试时发现录到一半就停了,查了半天才发现是后台限制导致的。

嗯,屏幕录制这块内容其实不算复杂,但细节特别多。尤其是权限和虚拟显示这块,稍不注意就会出问题。我个人建议:先把核心流程跑通,再逐步优化性能和兼容性。别一上来就想搞个完美的录制工具,先让画面能录下来再说。


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