前言:由于最近项目中需要使用到屏幕共享,所以对iOS屏幕共享进行了一番调研,在这里也分享下踩坑之路。
屏幕共享简介
屏幕共享是把屏幕上的内容分享给其他人观看,这分为两项关键技术:
- 屏幕内容采集:涉及到系统权限,以及需要系统提供采集屏幕内容的Api;(苹果提供的屏幕录制框架
ReplayKit
) - 流媒体服务:通过采集到的视频流以及音频流推送给流媒体服务器广播给用户;(通常是以RTC的推流形式进行处理)
屏幕共享的应用场景:
手机游戏直播、客服指导、商务会议、教学白板等;
屏幕共享整体流程如下:
- 触发录屏
- 准备工作
- 开始录屏
- 处理数据流(音频、视频等)
- ExtensionApp 数据共享到 MainApp中(或者直接对此数据进行操作)
- 结束录屏
屏幕共享采集:
- 添加Target
- 创建Broadcast Upload Extension
- 添加AppGroups
可以在苹果开发者官网申请AppGroupID,并且把相关的profile文件,证书相关联,也可以在Xcode中添加后,由Xcode自动生成;
添加AppGroups需要在主App中和拓展中都添加;
- 添加完成后
最终项目中会存在两个文件,AppGroupID要保持一致;
到这里就可以开始处理屏幕共享了。由于涉及到进程之间(主App与拓展App之间)的通信问题,所以这里采用通知的方式来处理开始、结束等事件;屏幕共享采集数据进程通信目前采用NSUserDefault
、Socket
等方式,但是苹果扩展App有50M的内存限制在先,如果不需要帧数太高,可以使用NSUserDefault
传输sampleBuffer。如果对屏幕共享要求很高(帧率高、分辨率高),可以直接在拓展App中直接上传流,或者使用socket。使用socket可以在一定程度上不依赖内存,但是需要处理帧堆积导致的内存爆增问题,可以避免扩展程序被系统强制KILL,但Socket有一定的不稳定性,需要额外处理断线以及网络异常等问题;
推荐的进程通信方式:
- 事件通知:
CFNotification
; - 简单的值传递:
NSUserDefault
; - 复杂的数据传递:
Socket
;
开始屏幕共享
这个方法有不确定性,🤷不知道未来某天是否还能使用,能用就先用吧!苹果并没有给出一个明确的方法,只有一个控件可以调起屏幕共享弹窗,所以这里是以一种取巧的方式封装起来的方法,给App调用;
在调起屏幕共享后,到我们真正开启屏幕共享,还有一步用户确认操作,所以我们需要知道屏幕共享扩展程序的事件回调后,再去处理某些逻辑,才能真正形成闭环;
// 系统弹窗
- (RPSystemBroadcastPickerView *)systemPicker {
if (!_systemPicker) {
RPSystemBroadcastPickerView* picker =
[[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 0, 44, 44)];
picker.showsMicrophoneButton = NO;
picker.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin;
_systemPicker = picker;
}
return _systemPicker;
}
- (void)launchBroadcaster API_AVAILABLE(ios(12.0)) {
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtPath:plugInPath error:nil];
for (NSString *content in contents) {
NSURL *url = [NSURL fileURLWithPath:plugInPath];
NSBundle *bundle = [NSBundle bundleWithPath:[url URLByAppendingPathComponent:content].path];
NSDictionary *extension = [bundle.infoDictionary objectForKey:@"NSExtension"];
if (extension == nil) { continue; }
NSString *identifier = [extension objectForKey:@"NSExtensionPointIdentifier"];
if ([identifier isEqualToString:@"com.apple.broadcast-services-upload"]) {
self.systemPicker.preferredExtension = bundle.bundleIdentifier;
break;
}
}
for (UIView *view in self.systemPicker.subviews) {
UIButton *button = (UIButton *)view;
[button sendActionsForControlEvents:UIControlEventAllTouchEvents];
}
}
停止屏幕共享
停止屏幕共享方法是由ReplayKit扩展中的提供的,从MainApp中无法直接调用,可以通过通知的方式去调用ExtensionApp 的方法;
// MainApp 发送通知给 ExtensionApp, 扩展程序收到通知调用停止方法
// MainApp
- (void)stopBroadcaster {
CFNotificationName notificationName = CFNotificationName(TScreenShareHostRequestStopNotification);
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true);
}
// ExtensionApp
// 监听通知
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
onHostRequestFinishBroadcast,
(__bridge CFStringRef)ScreenShareHostRequestStopNotification,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
static void onHostRequestFinishBroadcast(CFNotificationCenterRef center,
void *observer,
CFStringRef name,
const void *object,
CFDictionaryRef
userInfo) {
ScreenShareSampleHandler *self = (__bridge ScreenShareSampleHandler *)(observer);
NSError *error = [NSError errorWithDomain:NSStringFromClass(self.class)
code:0
userInfo:@{
NSLocalizedFailureReasonErrorKey:NSLocalizedString(@"您已停止屏幕共享。", nil)
}];
[self finishBroadcastWithError:error];
}
ExtensionApp事件回调
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
CFNotificationName notificationName = CFNotificationName(ScreenShareBroadcastStartedNotification);
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true);
}
- (void)broadcastPaused {
// User has requested to pause the broadcast. Samples will stop being delivered.
CFNotificationName notificationName = CFNotificationName(ScreenShareBroadcastPausedNotification);
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true);
}
- (void)broadcastResumed {
// User has requested to resume the broadcast. Samples delivery will resume.
CFNotificationName notificationName = CFNotificationName(ScreenShareBroadcastResumedNotification);
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true);
}
- (void)broadcastFinished {
// User has requested to finish the broadcast.
CFNotificationName notificationName = CFNotificationName(ScreenShareBroadcastFinishedNotification);
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true);
}
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
//使用NSUserDefault
}
MainApp监听屏幕共享事件通知
// 屏幕共享开始
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
onBroadcastStarted,
(__bridge CFStringRef)TScreenShareBroadcastStartedNotification,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
// 屏幕共享完成
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
onBroadcastFinished,
(__bridge CFStringRef)TScreenShareBroadcastFinishedNotification,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
// 屏幕共享暂停
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
onBroadcastPaused,
(__bridge CFStringRef)TScreenShareBroadcastPausedNotification,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
// 屏幕共享暂停
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
onBroadcastResumed,
(__bridge CFStringRef)TScreenShareBroadcastResumedNotification,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
// 实现方法
static void onBroadcastStarted(CFNotificationCenterRef center,
void *observer,
CFStringRef name,
const void *object,
CFDictionaryRef
userInfo) {
}
static void onBroadcastFinished(CFNotificationCenterRef center,
void *observer,
CFStringRef name,
const void *object,
CFDictionaryRef
userInfo) {
}
static void onBroadcastPaused(CFNotificationCenterRef center,
void *observer,
CFStringRef name,
const void *object,
CFDictionaryRef
userInfo) {
}
static void onBroadcastResumed(CFNotificationCenterRef center,
void *observer,
CFStringRef name,
const void *object,
CFDictionaryRef
userInfo) {
}
流媒体服务:
这里大多都是使用第三方的服务,并且每家SDK的方法都大同小异;RTC屏幕共享方法有开始屏幕共享,停止屏幕共享,以及推流等方法;基本上集成上都有傻瓜式教程,就不在这里展开叙述;
总结:
iOS系统实现屏幕共享的功能太曲折了,就ReplayKit
整体而言,对开发者并不是很友好。iOS版本之间兼容也是很头疼的,实现屏幕共享的细节还有很多,在屏幕共享采集后的处理也尤为关键,比如对视频帧和音频帧的处理;还有就是屏幕共享扩展的内存限制50M,这也是需要开发者特别注意的地方;