iOS屏幕共享
Pano SDK 从 1.3.9 版本开始,支持 iOS 屏幕共享(需要 iOS 11.0 及以上的系统版本),本文介绍如何接入该功能。
实现原理
iOS 上实现系统级屏幕共享需要通过 Broadcast Upload Extension 扩展配合主 App 进行。
Extension 负责接收屏幕数据,并传给主 App,由主 App 负责发送数据。
接入流程
配置 App Group
注意:不配置 App Group 也可以使用屏幕共享功能,但可能影响数据传输稳定性。建议配置 App Group 以获得最佳体验。
配置方法如下(以Xcode 12.4 为例):
- Xcode 选择主 App 的「TARGETS」,点击「Signing & Capabilities」,点击下方「+ Capability」。
- 在弹出框中双击选择「App Groups」,会自动展示 App Groups 项。
- 如果已展示 App Groups 列表,可以选择其中一个。如需创建新的 App Groups,在「App Groups」项底部,点击「+」(如果未选择 Development team,会提示选择,选好后点击「Select」),在弹出的「Add a new container」窗口输入自定义的 App Group ID(注意:系统会自动添加 group. 前缀),点击「OK」保存。
创建 Broadcast Upload Extension
- Xcode 点击「TARGETS」列表最底部(窗口最底部)的「+」按钮,在弹出窗口选择「Broadcast Upload Extension」。
- 点击「Next」,在弹出窗口中填写相关信息(Extension 的 Bundle Idenifier 需要和主 TARGET 不同),取消勾选「Include UI Extension」,点击「Finish」完成创建。
创建 Extension 时配置的 Product Name 会自动填充到 General - Identity - Display Name 中,在用户选择屏幕录制的界面上会作为应用名称显示,请按需修改。如果手工导入 Pano SDK,请将 SDK 包的 PanoReplayKitExt.framework 添加到 Extension 的 TARGET - General - Frameworks and Libraries,将 Embed 属性设置为 Embed & Sign。
注意:PanoReplayKitExt.framework 只需要添加到 Extension,不要添加到主工程。
如果通过 CocoaPods 自动集成 Pano SDK,请在 Podfile 中添加:
target 'YourMainTargetName' do use_frameworks! # 1.3.9 版本 SDK 开始支持屏幕共享 pod 'PanoRtc_iOS', '~> 1.3.9' target 'YourExtensionTargetName' do platform :ios, '11.0' use_frameworks! pod 'PanoRtc_iOS/ReplayKitExt' end end
- Extension 的 General - Deployment Info 中,可以选择版本「iOS 11.0」以匹配最大兼容性。
- (可选)如果已对主 App 配置 App Group,请参考前一节的介绍,对 Extension 的 TARGET 同样配置 App Group(Extension 的 App Group ID 需要和 主 TARGET 的 App Group ID 相同)。
配置 Extension 采集数据
创建 Extension 后,Xcode 会自动生成 SampleHandler
类文件,在其中引入 PanoReplayKitExt
头文件并按照下列说明编写代码,来完成数据采集逻辑。
// 导入 PanoReplayKitExt.h
#import <PanoReplayKitExt/PanoReplayKitExt.h>
// 实现 PanoScreenSharingExtDelegate 协议
@interface SampleHandler () <PanoScreenSharingExtDelegate>
@end
- (可选)如果配置了 App Group,在
SampleHandler.m
的broadcastStartedWithSetupInfo
中配置代理,将kAppGroupId
替换为 App Group ID:- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *>*)setupInfo { [PanoScreenSharingExt.sharedInstance setupWithAppGroup:kAppGroupId delegate:self]; }
- 在用户手动停止屏幕共享时通知
PanoReplayKitExt
结束共享:- (void)broadcastFinished { [PanoScreenSharingExt.sharedInstance finishScreenSharing]; }
- 在
processSampleBuffer
方法中将sampleBuffer
传递给PanoReplayKitExt
:- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType { switch (sampleBufferType) { case RPSampleBufferTypeVideo: // PanoReplayKitExt 只需接收 Video 数据 [PanoScreenSharingExt.sharedInstance sendVideoSampleBuffer:sampleBuffer]; break; case RPSampleBufferTypeAudioApp: break; case RPSampleBufferTypeAudioMic: break; default: break; } }
- 实现
PanoScreenSharingExtDelegate
的-screenSharingFinished:
回调,并处理相关事件:- (void)screenSharingFinished:(PanoScreenSharingResult)reason { NSString *log; switch (reason) { case PanoScreenSharingResultVersionMismatch: log = @"Pano SDK 和 PanoReplayKitExt 版本不匹配"; break; case PanoScreenSharingResultCloseByHost: log = @"屏幕共享被关闭"; break; case PanoScreenSharingResultDisconnected: log = @"连接异常断开"; break; default: break; } NSError *err = [NSError errorWithDomain:error.domain code:error.code userInfo:@{ NSLocalizedFailureReasonErrorKey : log }]; [self finishBroadcastWithError:err]; }
配置主 App 接收数据
在主 App 中使用 Pano SDK 开启屏幕共享,让主 App 准备接收 Extension 的录屏数据,按照如下步骤进行配置:
注意:使用 Extension 进行屏幕录制,当你的 App 切换到后台时,其他用户会看到你的屏幕共享处于最后一幅画面。为避免此问题,开发者需要让 App 常驻后台,例如,通过调用 Pano SDK 发送声音的接口,或者其他合适的方式。
- 确保调用屏幕共享接口前,已经成功加入频道。
- 调用
PanoRtcEngineKit
实例的- startScreenWithAppGroupId:
方法,传入 App Group ID(如果没有配置 App Group 则传入nil
)。 - 在
PanoRtcEngineDelegate
的- onScreenStartResult:
回调方法中判断屏幕共享状态,result == kPanoResultOK
表示开启成功。 - 等待用户通过 iOS 系统控制中心触发屏幕录制,或者参考下一步的介绍使用系统提供的
RPSystemBroadcastPickerView
来启动。
控制中心启动需要用户长按屏幕录制按钮,并在弹出列表中选择 Extension 名称来启动。 - (可选)通过系统提供的
RPSystemBroadcastPickerView
类可以实现从主 App 内唤起屏幕录制页面,将如下代码添加到需要启动屏幕录制的地方:注意:Apple 官方并不推荐此方案,可能随着系统更新被禁止,使用这种方式启动需要考虑此风险。
RPSystemBroadcastPickerView
需要 iOS 12 以上支持,目前不支持自定义界面,仅能唤起屏幕录制启动页面供用户操作。... #import <ReplayKit/ReplayKit.h> // 导入系统库 ... @property (strong, nonatomic) RPSystemBroadcastPickerView *broadcastPickerView; // 定义一个属性 ... [self launchBroadcastPickerView]; // 在需要启动屏幕共享的时机调用,以自动弹出屏幕录制提示 ... // 方法体 - (void)launchBroadcastPickerView API_AVAILABLE(ios(12.0)){ if (!self.broadcastPickerView) { RPSystemBroadcastPickerView *pickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 0, 44, 44)]; pickerView.showsMicrophoneButton = NO; pickerView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin; NSString *pluginPath = [NSBundle mainBundle].builtInPlugInsPath; NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:pluginPath error:nil]; for (NSString *content in contents) { if (![content hasSuffix:@".appex"]) { continue; } NSBundle *bundle = [NSBundle bundleWithPath:[[NSURL fileURLWithPath:pluginPath] URLByAppendingPathComponent:content].path]; if (bundle) { NSString *identifier = [bundle.infoDictionary valueForKeyPath:@"NSExtension.NSExtensionPointIdentifier"]; if ([identifier isEqualToString:@"com.apple.broadcast-services-upload"]) { pickerView.preferredExtension = bundle.bundleIdentifier; } } } self.broadcastPickerView = pickerView; } for (UIView *view in self.broadcastPickerView.subviews) { if ([view isKindOfClass:[UIButton class]]) { [(UIButton *)view sendActionsForControlEvents:UIControlEventAllEvents]; } } }
- 通过调用
PanoRtcEngineKit
实例的stopScreen
方法来停止屏幕共享,或者用户也可以通过系统控制中心来手工停止。
屏幕共享事件通知
通过 PanoRtcEngineDelegate
协议的回调方法来处理 Pano SDK 返回的通知。 和屏幕共享有关的事件通知如:
- onUserScreenStart:
其他用户开启屏幕共享- onUserScreenStop:
其他用户关闭屏幕共享- onUserScreenMute:
其他用户暂停屏幕共享- onUserScreenUnmute:
其他用户恢复屏幕共享- onUserScreenSubscribe:withResult:
订阅其他用户屏幕共享的结果(仅订阅失败时回调)- onFirstScreenFrameRendered:
收到其他用户的第一个屏幕共享画面- onScreenStartResult:
自己开启屏幕共享的结果
订阅其他用户的屏幕共享
通过 PanoRtcEngineKit
实例的 - subscribeScreen:withView:
或 - subscribeScreen:withDelegate:
方法订阅其他用户的屏幕共享。
前者随带渲染视图,后者随带外置渲染器。
取消订阅屏幕共享
通过 PanoRtcEngineKit
实例的 - unsubscribeScreen:
方法取消订阅其他用户的屏幕共享。
其他用户停止屏幕共享时,Pano SDK 会自动取消订阅,这种情况不必调用取消订阅方法。
示例代码
我们提供示例代码供开发者参考。