17、屏幕录制与投屏:MediaProjection API、屏幕采集、Miracast/AirPlay协议简介

屏幕录制和投屏,这两个功能现在几乎成了音视频App的标配。你想想看,从游戏直播到远程会议,从教学录屏到家庭影音共享,背后都离不开这套技术。今天我就带你把这套东西彻底捋一遍。

17.1 屏幕录制:MediaProjection API 深度解析

Android的屏幕录制,核心就是 MediaProjection 这套API。说白了,它就是一个「虚拟显示器」,能把屏幕上的像素数据实时抓取出来。

17.1.1 申请权限与获取MediaProjection

第一步,你得先让用户授权。Android出于安全考虑,不允许App偷偷录屏,必须弹窗让用户确认。

// 申请屏幕录制权限
private static final int REQUEST_CODE_SCREEN_CAPTURE = 1001;

private void startScreenCapture() {
    MediaProjectionManager projectionManager = 
        (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
    Intent intent = projectionManager.createScreenCaptureIntent();
    startActivityForResult(intent, REQUEST_CODE_SCREEN_CAPTURE);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE_SCREEN_CAPTURE && resultCode == RESULT_OK) {
        // 获取MediaProjection实例
        MediaProjectionManager projectionManager = 
            (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        MediaProjection mediaProjection = 
            projectionManager.getMediaProjection(resultCode, data);
        // 接下来就可以用这个实例去采集屏幕了
    }
}
我的经验: 记得在Android 10及以上,如果App在后台录屏,系统会强制停止。我有个项目就踩过这个坑——用户切到微信回个消息,回来发现录屏断了。解决方案是用前台Service保活。

17.1.2 屏幕采集:VirtualDisplay + Surface

拿到 MediaProjection 之后,怎么把屏幕内容取出来?答案是创建一个 VirtualDisplay,把屏幕内容渲染到一个 Surface 上。

// 创建VirtualDisplay
private VirtualDisplay createVirtualDisplay(MediaProjection mediaProjection, 
                                            int width, int height, Surface surface) {
    return mediaProjection.createVirtualDisplay(
        "ScreenCapture",
        width, height,  // 采集分辨率
        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
        surface,        // 输出目标
        null,           // 回调
        null            // Handler
    );
}

这个 Surface 可以来自 MediaCodec(编码成视频流),也可以来自 SurfaceView(实时预览)。我个人习惯用 MediaCodec 的输入Surface,这样采集和编码一步到位。

17.1.3 完整录屏流程:采集 → 编码 → 封装

一个完整的录屏流程,其实就是把上面几个环节串起来:

  1. 获取MediaProjection:用户授权
  2. 配置MediaCodec:设置编码格式(H.264/H.265)、码率、帧率
  3. 创建VirtualDisplay:绑定到MediaCodec的输入Surface
  4. 编码并写入MediaMuxer:生成MP4文件
// 配置MediaCodec编码器
MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
MediaFormat format = MediaFormat.createVideoFormat(
    MediaFormat.MIMETYPE_VIDEO_AVC, 1920, 1080);
format.setInteger(MediaFormat.KEY_BIT_RATE, 8_000_000);  // 8Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);  // 关键帧间隔1秒
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = encoder.createInputSurface();
encoder.start();

// 创建VirtualDisplay,绑定到编码器的输入Surface
VirtualDisplay virtualDisplay = createVirtualDisplay(mediaProjection, 1920, 1080, inputSurface);

// 编码后的数据写入MediaMuxer
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// ... 处理编码输出的Buffer,写入Muxer
注意: 我曾经在小米某款机型上遇到编码器输出时间戳错乱的问题。原因是 MediaCodec 输出的 BufferInfo.presentationTimeUs 需要自己校准,不能直接拿系统时间。建议用 System.nanoTime() 计算相对时间戳。

17.2 屏幕采集的进阶技巧

17.2.1 采集区域与分辨率控制

不是所有场景都需要全屏1080p。比如录教学视频,可能只需要录某个App窗口。这时候可以调整 VirtualDisplay 的宽高参数。

// 只采集屏幕左上角720x1280区域
VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay(
    "PartialCapture",
    720, 1280,  // 采集分辨率
    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
    surface,
    null, null
);

但要注意,VirtualDisplay 不支持指定采集原点,它总是从屏幕左上角开始。如果你想采集特定区域,得在编码后的数据上做裁剪。

17.2.2 音频采集:麦克风 vs 系统内录

录屏不带声音?那体验太差了。Android的音频采集分两种:

  • 麦克风录音:用 AudioRecord,采集外部声音
  • 系统内录:Android 10+ 支持通过 AudioPlaybackCapture API 采集App自身播放的声音

我建议做游戏录屏时,同时采集麦克风和系统内录,然后混音。这样既能听到游戏音效,又能录到解说。

// 系统内录配置(Android 10+)
AudioPlaybackCaptureConfiguration config = 
    new AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
        .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
        .build();
AudioRecord audioRecord = new AudioRecord.Builder()
    .setAudioPlaybackCaptureConfig(config)
    .build();

17.3 投屏协议:Miracast 与 AirPlay

投屏和录屏本质上是同一件事——都是把屏幕内容传输出去。区别在于:录屏是存成本地文件,投屏是实时发送到远端设备。

17.3.1 Miracast:Wi-Fi Direct + RTP

Miracast 是 Wi-Fi Alliance 推出的标准,基于 Wi-Fi Direct 点对点连接。说白了,就是手机和电视直接连,不经过路由器。

它的技术栈是这样的:

  • 底层:Wi-Fi Direct 建立P2P连接
  • 传输:RTP(实时传输协议)承载音视频数据
  • 编码:H.264 视频 + AAC 音频
  • 控制:RTSP 用于会话管理

Android从4.2开始就内置了Miracast的Source端支持(通过 WifiDisplay API),但说实话,这玩意儿兼容性不太好。我在测试时发现,三星电视和索尼电视的Miracast实现差异很大,有的能连上但卡顿,有的直接搜不到。

避坑指南: 我曾经在开发投屏功能时,发现Miracast在5GHz频段下延迟更低,但穿墙能力差。如果你的App需要跨房间投屏,建议强制使用2.4GHz。

17.3.2 AirPlay:苹果的私有协议

AirPlay 是苹果的投屏协议,Android端想用?得逆向工程。不过好在有开源实现,比如 AirPlayServer 库。

AirPlay 的核心特点:

  • 基于HTTP/2:控制信令走HTTP/2,媒体数据走UDP
  • ALAC音频:苹果无损音频编码,音质好
  • H.264/H.265视频:支持硬件编码
  • FairPlay DRM:苹果的版权保护,这个基本无解

如果你要在Android上实现AirPlay接收端,建议用 shairplayAirPlayKit 这类开源库。但注意,苹果经常更新协议,旧版本可能很快失效。

17.3.3 协议对比与选型建议

特性 Miracast AirPlay Google Cast
连接方式 Wi-Fi Direct 局域网 局域网
延迟 100-200ms 50-100ms 200-500ms
兼容性 中(各厂商实现不一) 高(仅苹果设备) 高(Android TV/Chromecast)
开源实现 有(WifiDisplay) 有(第三方库) 有(Google Cast SDK)
适用场景 Windows/Android投电视 苹果生态投屏 Android投Chromecast

我个人建议:如果是做Android投Android,用Miracast;如果是投苹果设备,老老实实用AirPlay;如果是投电视盒子,Google Cast最省心。

17.4 实战:实现一个简单的投屏Sender

最后,我们动手写一个投屏发送端的核心逻辑。假设目标设备已经通过某种方式(比如MDNS发现)获取到了IP地址。

// 投屏发送端核心逻辑
public class ScreenCastSender {
    private MediaProjection mediaProjection;
    private MediaCodec encoder;
    private VirtualDisplay virtualDisplay;
    private RtpSender rtpSender;  // 假设实现了RTP发送

    public void startCast(String targetIp, int port) {
        // 1. 配置编码器
        MediaFormat format = MediaFormat.createVideoFormat(
            MediaFormat.MIMETYPE_VIDEO_AVC, 1920, 1080);
        format.setInteger(MediaFormat.KEY_BIT_RATE, 5_000_000);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
        encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        Surface inputSurface = encoder.createInputSurface();
        encoder.start();

        // 2. 创建VirtualDisplay
        virtualDisplay = mediaProjection.createVirtualDisplay(
            "ScreenCast", 1920, 1080, 1,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
            inputSurface, null, null);

        // 3. 启动RTP发送线程
        rtpSender = new RtpSender(targetIp, port);
        new Thread(() -> {
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            while (!Thread.interrupted()) {
                int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
                if (outputIndex >= 0) {
                    ByteBuffer outputData = encoder.getOutputBuffer(outputIndex);
                    // 封装成RTP包发送
                    rtpSender.sendFrame(outputData, bufferInfo);
                    encoder.releaseOutputBuffer(outputIndex, false);
                }
            }
        }).start();
    }

    public void stopCast() {
        virtualDisplay.release();
        encoder.stop();
        encoder.release();
        rtpSender.stop();
    }
}
小技巧: 投屏时网络波动是常态。我习惯在编码器设置 KEY_I_FRAME_INTERVAL 为1秒,这样即使丢包,最多花1秒就能恢复画面。另外,建议用FEC(前向纠错)来对抗丢包,效果比单纯重传好得多。

嗯,屏幕录制和投屏这块,核心就是 MediaProjection + VirtualDisplay + 编码器这个铁三角。协议方面,Miracast适合点对点,AirPlay适合苹果生态。实际开发中,你可能会遇到各种兼容性问题——别慌,多抓包、多对比不同设备的实现,慢慢就能摸清规律。


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