iOS 屏幕共享功能实现
iOS 屏幕共享功能实现
本文档描述基于 EVSDK 实现 iOS 版本客户端应用的会议中屏幕共享功能,其中重点介绍了所使用到的 APP Extension 、EVSDK 等信息,此功能仅支持 iOS12 以上的系统。该功能的实现依赖于 iOS 的 ReplayKit 功能,有关背景内容可以参考 Apple 文档, https://developer.apple.com/videos/play/wwdc2018/601/ (opens new window), https://developer.apple.com/videos/play/wwdc2017/606/ (opens new window) 和 https://developer.apple.com/documentation/replaykit (opens new window)
前期准备工作
创建 Extensions Bundle Identifier
前往开发者网站 https://developer.apple.com/ (opens new window)
创建 App Groups, 在Certificates, Identifiers & Profiles 中选择 Identifiers,找到 App Groups 并创建,格式 group+ 域名翻转
//示例:
group.com.hexmeet
创建两个 Extension 的 Identifiers
注意
创建 Identifiers,格式前缀务必使用与主程序 bundleID 相对应的域名反写,并分别设置为 uploadext 、setupuiext
//示例 Identifiers:com.hexmeet.hjtdebug.uploadext
, com.hexmeet.hjtdebug.setupuiext
为 bundle identifier 增加 App Groups 设置
Extension 的 bundle identifier 创建完成后,查看详情,并在Capabilities 的 App Groups 中编辑,选择创建好的 App Group 名
然后,查看主程序的 bundle identifier 的 Capabilities 中同样勾选 App Groups,编辑并选择新创建的 App Group 名
创建相应的 Extension 描述文件,并更新主程序的描述文件 (因为主程序的 Capabilities 中增加了 App Groups 设置)
项目工程修改
程序中创建 APP Extension,设置相关配置
创建 tagert,选择 iOS 下的 Application Extension 模块下的 Broadcast Upload Extension,创建过程中如果勾选 Include UI Extension 则系统会自动生成 Broadcast Setup UI Extension。如果没有勾选,请再创建一个 Broadcast Setup UI Extension
创建完成之后,开始配置相关功能,选择主程序 target 的 Signing & Capabilities 点击 +Capability,增加 App Groups 功能,增加完成之后勾选之前创建好的 App Group 的 Identifiers。依次选择创建好的两个 Extension 并配置 App Groups,App Groups 的Identifiers 要选择和主程序 App Group 勾选的 Identifiers 一致。
APP Extension 使用
屏幕共享依赖于系统提供的 Broadcast Upload Extension 和 Broadcast Setup UI Extension
Broadcast Upload Extension 和 Container app 之间通讯以 APP Group 方式形式进行,此进程为主要收集原始音视频数据以及录制进程状态的通知
Broadcast Setup UI Extension 这个进程通常用于让用户输入一些信息来鉴权,或者自定义其他界面,在启动录制进程之间插入的一个交互的页面,这里可以不做操作使用系统提供的配置方法
接收原始音视频数据 (Broadcast Upload Extension 提供的 SampleHandler 回调类中),为了方便对原始数据进行内容处理,请将SampleHandler.m 文件修改成 SampleHandler.mm
音频内容原始数据接收方法
-(void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType: (RPSampleBufferType) sampleBufferType;
对原始数据进行处理:(复制此方法)
-(void)sendVideoBuffer:(CMSampleBufferRef)sampleBuffer;
Extension 和 Contain app之间通讯以 APP Group 方式形式进行:
-(void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo;
//示例代码
-(void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
_userDefault = [[NSUserDefaults alloc] initWithSuiteName: @"group.com.hexmeet"];
_containerURL = [[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier: @"group.com.hexmeet"] URLByAppendingPathComponent:@"Library/Caches/yuvframe"];
[_userDefault setBool:YES forKey:@"broadcastStarted"];
[_userDefault synchronize];
}
屏幕停止录制系统会通过:
-(void)broadcastFinished;
//示例代码
-(void)broadcastFinished {
[_userDefault setBool:YES forKey:@"broadcastFinished"];
[_userDefault synchronize];
}
注意
事项及说明
_userDefault 在屏幕录制共享的数据内容中,我们标记了3种状态方式:broadcastStarted、videobufferready、broadcastFinished,可参考 demo 用法
EVSDK、ReplayKit 使用
EVSDK 相关方法
-(int) sendContent;
-(int) setLocalContentSourceInfo:(EVContentSourceInfo *_Nonnull)info;
-(int) stopContent;
-(void) onContent:(EVContentInfo *_Nonnull)info;
当调用 -(int) sendContent 方法会在 SDK 提供的 - (void)onContent:(EVContentInfo * _Nonnull)info
中收到回调
注意
info.status == EVContentDenied 意为禁止分享 当前会议收到回调status为EVContentDenied,请主动关闭分享操作,具体使用细节请参照 demo
info.dir == EVStreamUpload 当前会议环境若处于双流状态,操作点击分享,收到回调 dir 为 EVStreamUpload,请关闭双流界面,具体使用细节请参照 demo
info.dir == EVStreamDownload 当前会议若处于主动分享状态时,收到回调 dir 为EVStreamDownload,请主动关闭分享操作,改为被动接收分享,具体使用细节请参照 demo
sendContent、 stopContent 、setLocalContentSourceInfo 具体使用细节请参照 demo 中提供的 startShareTimer() 和 stopShareTimer()方法
Replaykit 使用
录制 UI 界面我们使用 RPSystemBroadcastPickerView,iOS12 之后提供的录屏 UI 控件
//示例代码,注意broadcastPickerView.preferredExtension 为upload extension的bundleID
if #available(iOS 12, *) {
let broadcastPickerView = RPSystemBroadcastPickerView(frame:CGRect(x: 50, y: 50, width: 42, height: 40))
broadcastPickerView.tag = 4567
broadcastPickerView.showsMicrophoneButton = false
broadcastPickerView.preferredExtension = "\(getInfoString("CFBundleIdentifier"))\(".uploadext")"
self.view.addSubview(broadcastPickerView)
broadcastPickerView.isHidden = true
}
具体使用此控件方法:
//示例代码
func shareMGBtnAction(){
if shareMGLb.text == "video.startShare".localized {
//share
isSeletShare = true
if #available(iOS 12, *) {
var btn:UIButton = UIButton()
let broadcastPickerView:RPSystemBroadcastPickerView = self.view.viewWithTag(4567) as! RPSystemBroadcastPickerView
for item:UIView in broadcastPickerView.subviews {
if item.isKind(of: UIButton.classForCoder()) {
btn = item as! UIButton
}
}
if #available(iOS 13, *) {
btn.sendActions(for: UIControl.Event.touchUpInside)
}else{
btn.sendActions(for: UIControl.Event.touchDown)
}
startShareTimer()
}
}else{
stopShareTimer()
}
}
### func startShareTimer() 方法为录制数据内容和evsdk交互的主要方法
//示例代码
func startShareTimer() {
userDefault = UserDefaults.init(suiteName:"group.com.hexmeet")
userDefault.setValue(false, forKey: "broadcastFinished")
userDefault.setValue(false, forKey: "videobufferready")
userDefault.synchronize()
if shareTimer == nil {
shareTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(pushShareContent), userInfo: nil, repeats: true)
}
}
此方法 0.2S 即代表每秒 5 帧画面
在定时器的 pushShareContent 方法中对 extension 存储的数据进行处理并传给 EVSDK
//示例代码请参照demo: @objc func pushShareContent() {}
方法
停止录制时
//示例代码请参照demo : func stopShareTimer() {}
方法
注意
会议挂断请主动再次调用 func stopShareTimer() {}
方法,确保会议结束调用 func stopShareTimer() {}
方法