iOS端屏幕录制Replaykit项目实践

上一篇阐述了调研结果,而我们常用的应用场景就是录制屏幕内容,然后将内容分享给他人(直播或录播)。流程如下:
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];
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容