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);
// 接下来就可以用这个实例去采集屏幕了
}
}
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 完整录屏流程:采集 → 编码 → 封装
一个完整的录屏流程,其实就是把上面几个环节串起来:
- 获取MediaProjection:用户授权
- 配置MediaCodec:设置编码格式(H.264/H.265)、码率、帧率
- 创建VirtualDisplay:绑定到MediaCodec的输入Surface
- 编码并写入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+ 支持通过
AudioPlaybackCaptureAPI 采集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实现差异很大,有的能连上但卡顿,有的直接搜不到。
17.3.2 AirPlay:苹果的私有协议
AirPlay 是苹果的投屏协议,Android端想用?得逆向工程。不过好在有开源实现,比如 AirPlayServer 库。
AirPlay 的核心特点:
- 基于HTTP/2:控制信令走HTTP/2,媒体数据走UDP
- ALAC音频:苹果无损音频编码,音质好
- H.264/H.265视频:支持硬件编码
- FairPlay DRM:苹果的版权保护,这个基本无解
如果你要在Android上实现AirPlay接收端,建议用 shairplay 或 AirPlayKit 这类开源库。但注意,苹果经常更新协议,旧版本可能很快失效。
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适合苹果生态。实际开发中,你可能会遇到各种兼容性问题——别慌,多抓包、多对比不同设备的实现,慢慢就能摸清规律。