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() {} 方法

上次更新: 11/24/2020, 4:27:04 PM