14、屏幕共享:Android屏幕采集、iOS屏幕广播、跨平台实现
屏幕共享这个功能,说实话,是WebRTC里最容易被低估的硬骨头。很多人觉得不就是录个屏嘛,但真正做起来,Android和iOS两边的路子完全不同。我最早做这块时,差点被系统限制搞到怀疑人生。
今天咱们就把它拆开揉碎,从Android的MediaProjection,到iOS的ReplayKit,再到怎么用一套代码搞定两端。嗯,开始吧。
Android屏幕采集:MediaProjection的正确姿势
Android从5.0开始提供了MediaProjection API。但注意,它不像摄像头那样直接拿权限就行——你得先弹个系统对话框让用户确认。
核心流程:
- 通过MediaProjectionManager获取Intent
- 用startActivityForResult启动授权
- 在onActivityResult里拿到data,创建MediaProjection实例
- 用VirtualDisplay把屏幕内容投到SurfaceTexture或Surface
代码大概长这样:
// 1. 获取管理器
MediaProjectionManager manager = (MediaProjectionManager)
getSystemService(Context.MEDIA_PROJECTION_SERVICE);
// 2. 发起授权
Intent intent = manager.createScreenCaptureIntent();
startActivityForResult(intent, REQUEST_CODE);
// 3. 回调中创建实例
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
MediaProjection projection = manager.getMediaProjection(resultCode, data);
// 开始采集
startScreenCapture(projection);
}
}
// 4. 创建VirtualDisplay
private void startScreenCapture(MediaProjection projection) {
Surface surface = /* 从EGLSurface或SurfaceTexture获取 */;
projection.createVirtualDisplay(
"ScreenShare",
width, height, dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface,
null, null
);
}
这里有个坑,我踩过好几次——VirtualDisplay的宽高必须和屏幕实际分辨率匹配,否则画面会拉伸或裁剪。我曾经图省事直接写死了1080p,结果在平板上一跑,画面歪得没法看。
我的习惯:用WindowManager获取当前屏幕的真实尺寸,再根据编码器的能力做缩放。别直接用DisplayMetrics的宽高,那可能包含导航栏。
iOS屏幕广播:ReplayKit的坑与解
iOS这边就麻烦多了。从iOS 12开始,苹果推荐用ReplayKit 2来做屏幕共享。但它的设计思路和Android完全不同——它走的是Extension(扩展)机制。
说白了,你的主App和屏幕采集不在同一个进程里。采集是在一个叫"Broadcast Upload Extension"的独立进程中跑的。这就带来两个问题:
- Extension和主App之间没法直接共享内存
- Extension的资源受限,不能做太多计算
我刚开始做时,傻乎乎地在Extension里直接做视频编码,结果跑几分钟就被系统杀掉。后来才明白,Extension只负责把原始数据传给主App,编码和推流应该交给主进程。
实现步骤:
- 新建一个Broadcast Upload Extension Target
- 在SampleHandler里实现processSampleBuffer
- 用Socket或CFMessagePort把数据传给主App
- 主App收到后,送入WebRTC的VideoSource
关键代码:
// Extension端 - SampleHandler.m
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer
withType:(RPSampleBufferType)sampleBufferType {
if (sampleBufferType == RPSampleBufferTypeVideo) {
// 通过Socket发送到主App
[self.socket sendData:[self convertSampleBufferToData:sampleBuffer]];
}
}
// 主App端 - 接收并送入WebRTC
- (void)didReceiveVideoFrame:(NSData *)frameData {
RTCMutableI420Buffer *buffer = [self decodeFrameData:frameData];
RTCVideoFrame *videoFrame = [[RTCVideoFrame alloc] initWithBuffer:buffer
rotation:0
timeStampNs:0];
[self.videoSource capturer:nil didCaptureVideoFrame:videoFrame];
}
注意:Extension的启动有延迟,用户点击"开始广播"后,可能要等1-2秒才能收到第一帧。我建议在UI上做个loading动画,别让用户干等。
跨平台实现:抽象一层接口
既然Android和iOS的实现差异这么大,怎么用一套代码搞定?我的做法是——抽象一个ScreenCapturer接口。
接口定义:
public interface ScreenCapturer {
void startCapture(ScreenCaptureCallback callback);
void stopCapture();
boolean isCapturing();
void setVideoSource(Object source); // 对应WebRTC的VideoSource
}
// 回调
public interface ScreenCaptureCallback {
void onFrameCaptured(byte[] data, int width, int height, int rotation);
void onError(String message);
}
Android实现:
public class AndroidScreenCapturer implements ScreenCapturer {
private MediaProjection projection;
private VirtualDisplay display;
private EGLBase eglBase;
@Override
public void startCapture(ScreenCaptureCallback callback) {
// 用MediaProjection + VirtualDisplay采集
// 通过EGL读取帧数据
}
}
iOS实现:
public class iOSScreenCapturer implements ScreenCapturer {
private SocketServer socketServer;
private RTCVideoSource videoSource;
@Override
public void startCapture(ScreenCaptureCallback callback) {
// 启动Socket监听,等待Extension发数据
// 收到后回调callback
}
}
这样,上层的业务代码完全不用关心底层是Android还是iOS。调用方只需要:
ScreenCapturer capturer = PlatformFactory.createScreenCapturer();
capturer.setVideoSource(peerConnectionFactory.createVideoSource(false));
capturer.startCapture(frame -> {
// 帧数据直接送入WebRTC pipeline
});
我的建议:在跨平台层,把帧的格式统一成I420。Android端从ImageReader读出来是YUV,iOS端从CMSampleBuffer读出来是NV12,统一转成I420后,上层处理就简单多了。
知识体系总览
下面这张图,是我自己整理的核心逻辑。你看一眼,基本就明白整个屏幕共享的脉络了:
性能优化与避坑
屏幕共享比摄像头采集更吃性能,因为分辨率通常更高(1080p甚至2K)。我总结了几条经验:
- 降分辨率:别直接采集原始分辨率。Android端用VirtualDisplay时指定一个较小的尺寸,比如720p。iOS端在Extension里用CVPixelBuffer的裁剪功能。
- 帧率控制:屏幕内容变化不频繁时,没必要跑30fps。我习惯动态调整——检测到画面变化大时提帧率,静止时降到5fps。
- 内存管理:Android的ImageReader如果不及时释放,会积压几十帧,内存暴涨。记得每帧处理完就调用image.close()。
我曾经踩过的坑:在Android上,如果用户旋转屏幕,VirtualDisplay不会自动跟随。你得监听ConfigurationChanged,销毁旧的VirtualDisplay,用新尺寸重新创建。否则画面方向就错了。
嗯,屏幕共享这块,说白了就是两套系统API + 一层抽象。Android那边直接,但要注意权限和尺寸;iOS那边绕,但Extension机制其实更安全。把接口统一好,上层代码几乎不用改。这就是跨平台的价值。
公众号:蓝海资料掘金营,微信deep3321