15、屏幕共享:屏幕采集API、MediaProjection使用、屏幕流编码与传输

屏幕共享,说白了就是把手机屏幕上的内容实时分享给远端的人。这在视频会议里是个刚需——你想想看,演示PPT、展示代码、远程协助,哪个离得开它?

我在做第一个WebRTC会议APP时,就踩过屏幕共享的坑。当时觉得不就是录个屏嘛,结果发现权限、编码、传输,每一步都有讲究。今天咱们就把这块彻底捋清楚。

15.1 屏幕采集的核心API

Android平台做屏幕采集,绕不开两个关键类:MediaProjectionManagerMediaProjection。前者负责申请权限,后者负责拿到屏幕数据。

嗯,这里要注意:屏幕采集需要用户明确授权。你不能偷偷录屏,这是Android安全机制的红线。

核心流程:

  1. 通过系统服务获取 MediaProjectionManager
  2. 发起权限请求Intent
  3. onActivityResult 中获取授权结果
  4. 创建 MediaProjection 实例
  5. 创建 VirtualDisplay 将屏幕内容输出到Surface
// 获取MediaProjectionManager
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) {
        if (resultCode == RESULT_OK) {
            // 创建MediaProjection实例
            MediaProjection projection = 
                projectionManager.getMediaProjection(resultCode, data);
            // 接下来就可以创建VirtualDisplay了
        }
    }
}

我个人习惯把这段逻辑封装成一个工具类。因为每次写重复的权限请求代码,真的很烦。

15.2 MediaProjection的实战用法

拿到 MediaProjection 对象后,下一步就是创建 VirtualDisplay。这个VirtualDisplay就像一块虚拟屏幕,你把内容投上去,它就会通过Surface输出。

我记得第一次用的时候,搞混了 VirtualDisplay 的宽高和屏幕实际分辨率。结果录出来的画面比例不对,远端看到的画面被拉伸了。后来我学乖了,直接用 DisplayMetrics 获取真实尺寸。

// 获取屏幕尺寸
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
int width = metrics.widthPixels;
int height = metrics.heightPixels;
int density = metrics.densityDpi;

// 创建VirtualDisplay
VirtualDisplay virtualDisplay = projection.createVirtualDisplay(
    "ScreenShare",
    width, height, density,
    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
    surface,  // 这个surface来自MediaCodec或SurfaceTexture
    null,
    null
);

小技巧: 如果你只需要分享部分区域(比如某个应用窗口),可以调整VirtualDisplay的宽高参数。但要注意,Android没有原生API支持窗口级采集,需要配合辅助功能服务来实现。

15.3 屏幕流的编码与传输

屏幕数据拿到手了,怎么传给远端?答案是用 MediaCodec 编码成视频流,然后通过WebRTC的 VideoTrack 发送出去。

这里有个关键点:屏幕共享的编码参数和摄像头不一样。摄像头追求低延迟,屏幕共享追求高清晰度。你想想看,PPT上的小字如果糊了,远端根本看不清。

参数 摄像头推荐 屏幕共享推荐
码率 1-2 Mbps 3-8 Mbps
帧率 30 fps 15-24 fps
分辨率 640x480 / 1280x720 1920x1080(原分辨率)
编码器 H.264 / VP8 H.264 / H.265(硬件编码)

我在项目中遇到过一个问题:屏幕共享时CPU占用飙升。后来发现是用了软件编码器。换成硬件编码器后,CPU占用从80%降到了15%。所以,能用硬件编码就别用软件。

// 配置MediaCodec用于屏幕编码
MediaFormat format = MediaFormat.createVideoFormat(
    MediaFormat.MIME_TYPE_AVC, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, 4_000_000);  // 4 Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 20);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);  // 关键帧间隔2秒
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 
    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);

MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = encoder.createInputSurface();
encoder.start();

// 将inputSurface传给VirtualDisplay
virtualDisplay = projection.createVirtualDisplay(
    "ScreenShare", width, height, density,
    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
    inputSurface, null, null
);

避坑指南: 我曾经在华为手机上遇到编码器崩溃的问题。原因是某些厂商的硬件编码器对 COLOR_FormatSurface 支持不完整。解决方案是降级到 COLOR_FormatYUV420Flexible,但需要手动做颜色空间转换。嗯,这个坑我踩了整整两天。

15.4 与WebRTC VideoTrack的对接

编码后的数据怎么喂给WebRTC?最直接的方式是自定义 VideoSource。WebRTC提供了 VideoSource 的基类,你只需要实现 onFrameCaptured 方法。

// 创建自定义VideoSource
VideoSource videoSource = peerConnectionFactory.createVideoSource(false);

// 创建VideoTrack
VideoTrack videoTrack = peerConnectionFactory.createVideoTrack(
    "screen_track_id", videoSource);

// 将编码后的帧送入VideoSource
// 注意:这里需要将MediaCodec输出的ByteBuffer转为VideoFrame
VideoFrame videoFrame = convertToVideoFrame(encodedData, width, height);
videoSource.onFrameCaptured(videoFrame);

// 将VideoTrack添加到PeerConnection
peerConnection.addTrack(videoTrack);

不过说实话,我更推荐用 SurfaceTextureHelper 来做这个对接。它内部处理了纹理转换和线程同步,比自己手动转 VideoFrame 省心得多。

15.5 屏幕共享的完整架构

来,我画了一张图,把整个流程串起来。你看完就明白了。

屏幕共享完整流程架构图 MediaProjection 屏幕采集授权 VirtualDisplay 屏幕内容 → Surface MediaCodec H.264硬件编码 VideoSource / VideoTrack 封装为WebRTC视频轨道 PeerConnection RTP传输到远端 关键参数配置 • 码率:4-8 Mbps • 帧率:15-24 fps • 分辨率:原始分辨率 • 编码器:硬件H.264 • 关键帧间隔:2秒 • 颜色格式:Surface 注意:MediaProjection需要用户授权,且Android 10+需要FOREGROUND_SERVICE权限

从图里你能看到,整个链路其实不复杂。但每个环节都有坑:权限申请、编码参数、颜色格式、线程安全……任何一个地方出问题,屏幕共享就挂了。

15.6 性能优化与注意事项

最后,我总结几条实战经验,你直接拿去用:

  • 分辨率降级: 如果设备性能不足,可以降低分辨率。比如从1080p降到720p,码率从4M降到2M。我在低端机上试过,效果还行。
  • 动态码率: 根据网络状况动态调整码率。画面静止时降低码率,画面变化剧烈时提高码率。WebRTC的 BitrateSettings 可以帮你做这个。
  • 关键帧间隔: 屏幕共享建议设置2-3秒一个关键帧。太短浪费带宽,太长远端加入时等待太久。
  • 内存管理: 屏幕采集会产生大量帧数据。记得及时释放 ByteBuffer,不然内存会暴涨。我见过一个案例,内存从200M涨到1.2G,然后APP被系统杀了。

个人建议: 如果你做的是企业级会议APP,建议加上屏幕共享的预览功能。让用户看到自己正在分享什么,避免尴尬。这个预览可以用 SurfaceView 绑定到VirtualDisplay的另一个Surface上实现。

好了,屏幕共享这块就讲到这里。核心就是三件事:拿到权限、编码数据、传给WebRTC。你动手写一遍,遇到问题再回来看这篇文章,应该就能搞定了。


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