17、Android屏幕采集与录屏推流:MediaProjection API使用、屏幕采集编码、录屏推流实战
屏幕采集,说白了就是把手机屏幕上的画面实时抓下来。这在直播、录屏教学、游戏直播里太常见了。我最早接触这块是在做一个远程协助App的时候,当时踩了不少坑,今天我把这些经验都摊开来跟你聊聊。
17.1 MediaProjection API 使用
Android从5.0开始提供了MediaProjection这套API,专门用来采集屏幕内容。嗯,这里要注意,它不是直接给你一个Surface让你画,而是通过VirtualDisplay来中转。
17.1.1 申请权限
屏幕采集需要用户明确授权。你不能偷偷摸摸录屏,这是Android的安全底线。
// 启动权限申请
MediaProjectionManager projectionManager =
(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
Intent intent = projectionManager.createScreenCaptureIntent();
startActivityForResult(intent, REQUEST_CODE);
用户同意后,你会拿到一个Intent,用它来创建MediaProjection实例。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
MediaProjectionManager projectionManager =
(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
MediaProjection mediaProjection =
projectionManager.getMediaProjection(resultCode, data);
// 拿到mediaProjection,后面用它创建VirtualDisplay
}
}
mediaProjection.stop()。
17.1.2 创建VirtualDisplay
VirtualDisplay是连接MediaProjection和编码器的桥梁。你把屏幕内容投到这个虚拟显示器上,编码器从Surface读取数据。
// 创建VirtualDisplay
mediaProjection.createVirtualDisplay(
"ScreenCapture",
width, // 屏幕宽度
height, // 屏幕高度
dpi, // 屏幕密度
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface, // 编码器的输入Surface
null, // 回调
null // Handler
);
这里有个坑:width和height必须是16的倍数,否则编码器会报错。我刚开始没注意这个,调试了半天才发现。
17.2 屏幕采集编码
拿到屏幕数据后,下一步就是编码。Android推荐用MediaCodec来做硬件编码,效率高、功耗低。
17.2.1 配置MediaCodec
我习惯用MediaCodec.createEncoderByType来创建编码器,指定MIME类型为"video/avc"(H.264)。
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000); // 2Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); // 关键帧间隔2秒
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = encoder.createInputSurface();
encoder.start();
17.2.2 编码流程
编码流程其实很简单:VirtualDisplay把画面渲染到Surface上,MediaCodec从Surface读取数据并编码。你只需要在编码输出端拿到数据就行。
// 从编码器取出编码后的数据
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
while (outputIndex >= 0) {
ByteBuffer outputBuffer = encoder.getOutputBuffer(outputIndex);
// 处理编码后的数据,比如写入文件或推流
byte[] data = new byte[bufferInfo.size];
outputBuffer.get(data);
// 这里可以回调给推流模块
encoder.releaseOutputBuffer(outputIndex, false);
outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
}
嗯,这里要注意:编码器输出的数据是H.264裸流,包含SPS/PPS等参数。推流时需要把这些参数提取出来,放到FLV的header里。
17.3 录屏推流实战
把编码后的数据推送到RTMP服务器,这才是完整流程。我一般用librtmp或者自己封装一个简单的推流器。
17.3.1 推流架构
整个流程可以画成一张图,方便理解:
17.3.2 推流核心代码
推流时,你需要把编码后的H.264数据封装成FLV格式,然后通过RTMP发送。这里我给出一个简化版的推流器:
public class RTMPSender {
private long rtmpHandle;
private boolean isConnected = false;
public boolean connect(String url) {
// 初始化RTMP连接
rtmpHandle = nativeInit();
int ret = nativeConnect(rtmpHandle, url);
isConnected = (ret == 0);
return isConnected;
}
public void sendVideoPacket(byte[] data, int size, long timestamp, boolean isKeyFrame) {
if (!isConnected) return;
// 封装成FLV VideoTag
byte[] flvPacket = createFlvVideoTag(data, size, timestamp, isKeyFrame);
nativeSendPacket(rtmpHandle, flvPacket, flvPacket.length);
}
private byte[] createFlvVideoTag(byte[] h264Data, int size, long timestamp, boolean isKeyFrame) {
// FLV VideoTag头:帧类型 + 编码ID + 视频数据
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
// FrameType: 1=关键帧, 2=非关键帧
int frameType = isKeyFrame ? 0x17 : 0x27;
bos.write(frameType); // 帧类型 + AVC编码
bos.write(0x01); // AVC packet type: 0=AVC sequence header, 1=AVC NALU
// 写入时间戳(相对时间)
bos.write(intToBytes((int)(timestamp & 0xFFFFFF)));
// 写入H.264数据
bos.write(h264Data, 0, size);
} catch (IOException e) {
e.printStackTrace();
}
return bos.toByteArray();
}
// JNI方法
private native long nativeInit();
private native int nativeConnect(long handle, String url);
private native int nativeSendPacket(long handle, byte[] data, int len);
private native void nativeClose(long handle);
}
- 关键帧(IDR帧)必须完整发送,否则解码端会花屏
- SPS/PPS需要在推流开始时发送一次,作为AVC sequence header
- 时间戳要递增,不能回退,否则播放器会卡住
17.3.3 音视频同步
录屏推流通常还需要采集音频。音频和视频的时间戳必须对齐,否则会出现音画不同步。我一般以音频时间戳为基准,视频时间戳尽量靠近它。
// 音频采集和编码
MediaCodec audioEncoder = createAudioEncoder();
// 视频编码器
MediaCodec videoEncoder = createVideoEncoder();
// 在推流时,确保时间戳对齐
long audioTimestamp = audioEncoder.getOutputTimestamp();
long videoTimestamp = videoEncoder.getOutputTimestamp();
// 如果视频落后太多,可以丢帧
if (videoTimestamp - audioTimestamp > 100) {
// 丢弃当前视频帧
continue;
}
17.4 性能优化与避坑
屏幕采集推流对性能要求很高,尤其是游戏直播场景。我总结几个关键点:
- 分辨率选择:不要直接用屏幕原始分辨率,720p或1080p就够了。4K录屏功耗高、带宽大,实际效果提升有限。
- 帧率控制:30fps是主流,60fps对编码器压力大。我做过测试,30fps和60fps在手机上看差别不大。
- 内存管理:编码器输出的ByteBuffer要及时处理,不要积压。我曾经因为处理不及时导致OOM。
- 网络适配:根据网络状况动态调整码率和帧率。弱网时降低码率,避免卡顿。
好了,屏幕采集与录屏推流的核心内容就这些。从MediaProjection申请权限,到VirtualDisplay创建,再到MediaCodec编码和RTMP推流,每一步都有细节需要注意。动手试试吧,遇到问题欢迎交流。