iOS端屏幕录制Replaykit项目实践

作者: 杭研融合通信iOS | 来源:发表于2018-09-04 11:06 被阅读215次

    上一篇阐述了调研结果,而我们常用的应用场景就是录制屏幕内容,然后将内容分享给他人(直播或录播)。流程如下:
    1.被录制端host app需引入 ReplayKit,以便可以使用其api选择一个app的extension来启动录制;
    2.广播端宿主app需要集成 Broadcast UI 和 Broadcast Upload 两个 Extension,以便出现在被录制端可选的 App 列表中;
    3.host app选定宿主app 后,将启动宿主app的extension,开始录制和广播相关逻辑。

    上文已经提到,从iOS9系统开始,苹果推出了replaykit 这个sdk来支持屏幕录制,通过extension形式实现屏幕录制。本文将对屏幕录制使用replaykit的技术细节进行描述, 下一篇将对录制内容的推送(广播)进行描述。通过本文你将对以下几方面得到信息:

    1. extension是什么?
    2. extension跟app什么关系?
    3. 在iOS10 11上集成extension注意哪些,区别有哪些?
    4. 调试时注意哪些?
    5. 调试时涉及到的原理和通信方式
    


    extension是什么?

    1. 逻辑形式:
      extension必须寄生在宿主app中,会随着宿主 app的安装而安装,同时随着宿主 app的卸载而卸载,但是extension却可以独立生存,即使宿主app没有启动,extension也可以为其他app提供相关服务。(能够调起extension的app被称为host app)
    2. 物理形式:
      iOS系统提供屏幕录制和直播功能都需要通过Extensions的形式来支持,通过在Xcode的已有工程中新建target,选择broadcast upload extension,这样工程中将自动添加broadcast upload extension和broadcast setup UI extension两个extensions。extension并不是一个独立的app,它有一个包含在app bundle中的独立bundle,extension的bundle后缀名是.appex。

    集成extension

    集成方式很简单,新建target,选择upload相关两个extension。集成之后将在工程的列表中看到两个新增的目录。
    需要注意的是,ios10 系统在upload的extension中的info.plist中NSExtensionPointIdentifier对应的value必须使用NSExtensionPointIdentifierkey对应ios10才兼容的com.apple.broadcast-services,不应该使用com.apple.broadcast-services-upload ,在iOS10系统中使用com.apple.broadcast-services-upload将无法通过编译,Xcode会报错。

    通信

    iOS10系统和iOS11系统的屏幕录制和直播,涉及到extensions和host app、containing app之间的通信,其中host app一端需要集成ReplayKit2,从而可以发起录制和直播请求,而containing app需要集成extensions,实现对其他可以录制的app的直播功能的支持。extension和host app之间可以通过extensionContext属性直接通信,extension和宿主containing app之间是通过IPC或基于group的文件共享来实现的。

    对于iOS10和iOS11,屏幕录制区别较大,前者只能录制app内的内容,后者可以录制整个系统的内容,而且前者可以通过代码控制录制的启动,而后者只能通过用户的操作(控制中心,点击圆点,选择app)启动录制。

    iOS 10

    在iOS10系统中,想要录制当前app内的内容,必须通过其他app的extension,而启动这个extension必须通过集成replaykit的api。

    @interface RPBroadcastActivityViewController : UIViewController
    + (void)loadBroadcastActivityViewControllerWithHandler:(void(^)(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error))handler;
    + (void)loadBroadcastActivityViewControllerWithPreferredExtension:(NSString * _Nullable)preferredExtension handler:(nonnull void(^)(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error))handler API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvos);
    @end
    
    @protocol RPBroadcastActivityViewControllerDelegate <NSObject>
    - (void)broadcastActivityViewController:(RPBroadcastActivityViewController *)broadcastActivityViewController didFinishWithBroadcastController:(nullable RPBroadcastController *)broadcastController error:(nullable NSError *)error API_AVAILABLE(ios(10.0), tvos(10.0));
    @end
    

    按照前文流程,当host app一端想要将app或系统内容广播给他人观看时,需要首先选择一个app的extension来帮他广播,就是需要展示出支持广播的app列表。这点通过调用ReplayKit2的RPBroadcastActivityViewController类的load相关api来实现。可以看到上面有两个api可供使用。

    • (void)loadBroadcastActivityViewControllerWithHandler:(void(^)(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error))handler;
      这时iOS系统将会寻找系统内已经集成了屏幕录制和直播extensions的containing app,并将这些app列表展示出来,用户可以在列表中选择containing app,点击选择之后,将通过containing app的extension中的UI-extension来展示相关的界面(可以自定义),让用户输入信息,一般用来鉴权或者保存用户信息,用户点击ok按钮之后,可以通过相关方法来调用[self.extensionContext completeRequestWithBroadcastURL:broadcastURL broadcastConfiguration:broadcastConfig setupInfo:setupInfo];,这个方法中将传递一些信息给host app,RPBroadcastActivityViewControllerDelegate的代理方法didFinishWithBroadcastController将会回调调用,这时我们可以获取到用于广播的controller,相当于与containing app已经建立起了通信链路,然后调用broadcastController 的startBroadcastWithHandler接口即可启动录制。

      ![ image.png
    • (void)loadBroadcastActivityViewControllerWithPreferredExtension:(NSString * _Nullable)preferredExtension handler:(nonnull void(^)(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error))handler
      第二个api是ios11新增的。可以通过参数preferredExtension,直接打开指定使用的app,只需要preferredExtension传递相应app extension的bundle id。RPBroadcastActivityViewControllerDelegate的代理方法didFinishWithBroadcastController将会回调调用,这时我们可以获取到用于广播的controller,相当于与containing app已经建立起了通信链路,然后调用broadcastController 的startBroadcastWithHandler接口即可启动录制。


      image.png

    iOS11

    在iOS10系统中,只能用户自己手动启动录制,并且无法通过代码控制录制进程的启动,所以被录制端host app其实无需集成replaykit,而只需要宿主app集成两个extension。
    与iOS10不同的是,用户手动选择录制app后,宿主app的extension相关方法将自动开始回调。

    录制进程

    通过上面的形式,启动录制后,我们可以在extension中自建出来的SampleHandler文件中相关代理方法中获取到屏幕采集的进度,具体使用方式见注释:

    @interface RPBroadcastSampleHandler : RPBroadcastHandler
    
    /*! @abstract Method is called when the RPBroadcastController startBroadcast method is called from the broadcasting application.
        @param setupInfo Dictionary that can be supplied by the UI extension to the sample handler.
      屏幕采集工作已经开始启动,在此方法中一般进行初始化工作
     */
    - (void)broadcastStartedWithSetupInfo:(nullable NSDictionary <NSString *, NSObject *> *)setupInfo;
    
    /*! @abstract Method is called when the RPBroadcastController pauseBroadcast method is called from the broadcasting application. */
    - (void)broadcastPaused;
    
    /*! @abstract Method is called when the RPBroadcastController resumeBroadcast method is called from the broadcasting application. */
    - (void)broadcastResumed;
    
    /*! @abstract Method is called when the RPBroadcastController finishBroadcast method is called from the broadcasting application. */
    - (void)broadcastFinished;
    
    /*! @abstract Method is called when broadcast is started from Control Center and provides extension information about the first application opened or used during the broadcast.
        @param applicationInfo Dictionary that contains information about the first application opened or used buring the broadcast.
     */
    - (void)broadcastAnnotatedWithApplicationInfo:(NSDictionary *)applicationInfo API_AVAILABLE(ios(11.2)) API_UNAVAILABLE(tvos);
    
    /*! @abstract Method is called as video and audio data become available during a broadcast session and is delivered as CMSampleBuffer objects.
        @param sampleBuffer CMSampleBuffer object which contains either video or audio data.
        @param sampleBufferType Determine's the type of the sample buffer defined by the RPSampleBufferType enum.
    采集到数据的实时回调,此方法中的sampleBuffer数据结构中有视频和音频数据,我们通过相关推流方法将数据推送给服务器,即实现了录制和推流。
     */
    - (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType;
    
    /*! @abstract Method that should be called when broadcasting can not proceed due to an error. Calling this method will stop the broadcast and deliver the error back to the broadcasting app through RPBroadcastController's delegate.
        @param error NSError object that will be passed back to the broadcasting app through RPBroadcastControllerDelegate's broadcastController:didFinishWithError: method.
     */
    - (void)finishBroadcastWithError:(NSError *)error;
    
    @end
    

    文件读写

    尽管extension的bundle是放在containing app的bundle中,但是他们是两个完全独立的进程,之间不能直接通信。不过extension可以通过openURL的方式启动containing app(当然也能启动其它app),不过extension中是无法直接使用openURL的,必须通过extensionContext借助host app来实现。extension和containing app可以共同读写一个被称为Shared resources的存储区域,这是通过App Groups实现的,用于同一group下的app共享同一份读写空间,以实现数据共享。
    • 首先需要在apple开发网站上对profile文件进行配置,将group数据共享配置,并设置group id(dns域名反写),用户app和extension之间;
    • 然后app中配置这个profile,并设置app的group,通过TARGETS-->App-->Capabilities-->App Groups,选择正确的group id;
    • 同时,在extension中也要通过TARGETS-->App-->Capabilities-->App Groups,选择同样的group id;
    • 通过NSUserDefaults共享数据,通过下面的形式:

    - (void)saveTextByNSUserDefaults
    {
        NSUserDefaults *shared = [[NSUserDefaults alloc]     initWithSuiteName:@"group.cmcc.ShareScreen"];
        [shared setObject:_textField.text forKey:@"cmcc"];
        [shared synchronize];
    }
    

    • 读写文件时,也需要通过指定group id的形式,才能将文件写入共享的数据区,或者从共享数据区读出来

    - (NSString *)readTextByNSFileManager
    {
        NSError *err = nil;
        NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.cmcc.ShareScreen "];
        containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"];
        NSString *value = [NSString stringWithContentsOfURL:containerURL encoding:NSUTF8StringEncoding error:&err];
        return value;
    }
    - (bool)writeTextByNSFileManager
    {
        NSError *err = nil;
        NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.cmcc.ShareScreen "];
        containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"];
        
        NSString *value = @"just test";
        BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&err];
        return result;
    }
    

    注意:containing app需要配置带有group配置的profile, extension可以配置自动,但是bundle id不能和containing app相同

    调试

    • 由于涉及到extensions作为独立target,所以调试时,需要单独编译运行,即我们想要调试containing app那就需要将xcode切换到containing app,然后重新运行,如果需要调试upload 或 setupUI的extension,那就需要需要切换到extension的target,在重新运行,这样才能在sampleHandler相关的方法中断点调试;


      image.png
    • userDidFinishSetup(通过extensionContext与host app通信的方法)必须在viewDidAppear后,而不能放在viewDidLoad之后,否则导致无法将事件传递给SampleHandler,它的代理方法不会回调。

    - (void)viewDidAppear:(BOOL)animated
    {
        [super viewDidAppear:animated];
        [self userDidFinishSetup];
    }
    

    相关文章

      网友评论

        本文标题:iOS端屏幕录制Replaykit项目实践

        本文链接:https://www.haomeiwen.com/subject/cncgwftx.html