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对象需要妥善管理生命周期。我曾经在项目里忘记释放它,导致屏幕闪烁、内存泄漏。记得在不需要时调用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
);

这里有个坑:widthheight必须是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();
小技巧:码率设置要根据场景调整。录屏内容变化快(比如游戏),建议设高一点,4-8Mbps;如果是静态画面,2Mbps就够了。我在做直播推流时,会根据网络状况动态调整码率。

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 推流架构

整个流程可以画成一张图,方便理解:

录屏推流架构图 屏幕采集 MediaProjection + VirtualDisplay H.264编码 MediaCodec + Surface输入 FLV封装 添加FLV Tag头 RTMP推流 关键点 • 分辨率需16的倍数 • 码率动态调整 • 关键帧间隔2秒 • 提取SPS/PPS • 音视频同步 • 网络抖动缓冲 • 断线重连机制 • 内存及时释放

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;
}
踩坑记录:我曾经在低端手机上遇到音视频不同步的问题。后来发现是音频采集线程被阻塞了。解决办法是使用独立的HandlerThread来处理音频,避免和UI线程竞争。

17.4 性能优化与避坑

屏幕采集推流对性能要求很高,尤其是游戏直播场景。我总结几个关键点:

  • 分辨率选择:不要直接用屏幕原始分辨率,720p或1080p就够了。4K录屏功耗高、带宽大,实际效果提升有限。
  • 帧率控制:30fps是主流,60fps对编码器压力大。我做过测试,30fps和60fps在手机上看差别不大。
  • 内存管理:编码器输出的ByteBuffer要及时处理,不要积压。我曾经因为处理不及时导致OOM。
  • 网络适配:根据网络状况动态调整码率和帧率。弱网时降低码率,避免卡顿。
我的习惯:在推流前先做一次网络测速,根据结果选择码率。如果网络波动大,我会启用自适应码率算法,每5秒调整一次。

好了,屏幕采集与录屏推流的核心内容就这些。从MediaProjection申请权限,到VirtualDisplay创建,再到MediaCodec编码和RTMP推流,每一步都有细节需要注意。动手试试吧,遇到问题欢迎交流。