概览
iOS 11新添加了用户从控制中心开启全局的录屏功能(在此之前只能实现应用内录制),这个功能和API(ReplayKit 2)的添加,使得录屏直播以及其他录屏相关的玩法拥有了无限可能. 刚好近一个月我司产品添加相关功能,并由我完成,从查阅相关文档到添加录屏直播的第一版App上线,用了2周多时间,期间尝试了阿里和腾讯的SDK,下面就回顾一下这段时间的学习和成长.
介绍Extension
Xcode 9中,添加了’Broadcast Upload Extension’ 和 ‘Broadcast Setup UI Extension’,用来实现相关功能,需要在File -> New -> Target中创建他们.
Broadcast Upload Extension
最为核心的是’Broadcast Upload Extension’(后简称’Upload’),‘Upload’中有多个系统回调方法,被用来告知录屏的开始,结束,暂停,恢复和采集到的音视频信息,大多数需求要通过该Extension实现. ‘Upload’独立于App存在,运行时也不依赖App,是一个独立的进程,也就是说,用户并不需要启动App,就可以使用录屏功能,拥有便捷,轻量的特点.
Broadcast Setup UI Extension
在创建’Broadcast Upload Extension’时,Xcode会询问你是否需要一并创建’Broadcast Setup UI Extension’(后称’Setup UI’),此处可以选择不创建. ‘Setup UI’在iOS11添加ReplayKit 2之前需要使用,在ReplayKit 2后,用户可以在控制中心发起全局的录屏功能后本插件就没那么重要了,由于本文主要介绍ReplayKit 2,故不做详细介绍了(我觉得这是个遗留问题,ReplayKit 2新特性更好更方便,完全可以替代之前的方案).
实践实现
类和方法
创建’Upload’Target后,Xcode中会创建SampleHandler类,是RPBroadcastSampleHandler的子类,类中会有如下方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo { // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional. } - (void)broadcastPaused { // User has requested to pause the broadcast. Samples will stop being delivered. } - (void)broadcastResumed { // User has requested to resume the broadcast. Samples delivery will resume. } - (void)broadcastFinished { // User has requested to finish the broadcast. } - (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType { switch (sampleBufferType) { case RPSampleBufferTypeVideo: // Handle video sample buffer break; case RPSampleBufferTypeAudioApp: // Handle audio sample buffer for app audio break; case RPSampleBufferTypeAudioMic: // Handle audio sample buffer for mic audio break; default: break; } } |
根据方法名和注释,显而易见的知道这些方法分别在录屏开始,暂停,恢复,结束和音视频数据是被调用,并且会传递一些信息. 简单说一下这几个方法:
-(void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo;
本方法在用户在 控制中心->屏幕录制->开始直播 点击321倒数结束后被调用.此处就可以开始初始化推流器,开启推流等工作,至于setupInfo,是’Setup UI’传递过来的一些初始化信息,当然如果不使用’Setup UI’,还是有多种方式传递’Upload’插件本身无法获取的信息.-(void)broadcastFinished;
本方法在用户 点击屏幕状态栏->系统弹窗->停止 后被调用.此处可以认为’Upload’插件进程即将被结束,需要做一些资源和内存释放等工作,然后插件进程结束.-(void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType;
本方法在-(void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo;
被调用后,会不停的被调用,用来获取采集到的音视频信息,通过sampleBufferType区分.此处可以将音视频信息编码推流.
消息传递与数据共享
前文说的,‘Upload’和App是相互独立的两个进程,‘Upload’和App如何进行消息传递和数据共享呢,大致有如下几种方式:
- 剪贴版:通过写入剪贴板,然后插件读取剪贴板传递一些简单数据,但是可靠性太差,可以作为一种辅助手段.
1 2 3 |
UIPasteboard *paste = [UIPasteboard generalPasteboard]; paste.string = @"a";// 写入 NSString *a = paste.string;// 读取 |
- 本地推送:插件可以发送本地通知,可以用来提示用户一些录屏事件,也可以提示用户点击通知,激活App做一些操作.
1 2 3 4 5 6 7 8 9 10 11 12 |
- (void)sendLocalNotificationToHostAppWithTitle:(NSString *)title msg:(NSString *)msg userInfo:(NSDictionary *)userInfo { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = [NSString localizedUserNotificationStringForKey:title arguments:nil]; content.body = [NSString localizedUserNotificationStringForKey:msg arguments:nil]; content.sound = [UNNotificationSound defaultSound]; content.userInfo = userInfo; UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1f repeats:NO]; UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"ReplayKitPush" content:content trigger:trigger]; [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { }]; } |
- 通知:CFNotificationCenter可以发送跨进程的通知,用来做一些实时事件交互.
1 2 |
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),kDarvinNotificationNamePushStart,NULL,nil,YES); CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), onDarwinReplayKitPushStart, kDarvinNotificationNamePushStart, NULL, CFNotificationSuspensionBehaviorDeliverImmediately); |
- AppGroup:开启AppGroup后,同一AppGroup的成员,可以共享沙盒数据,也就是说,App可以读取”Upload”的录屏文件,“Upload”可以从App获取必要信息.AppGroup需要在 Capabilities -> AppGroup 中开启,App和插件都要打开,后会生成.entitlements文件,会有一个group.BundleIdentifier的标识.
1 2 |
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroupId];// 共享的沙盒路径 NSUserDefaults *userDefault = [[NSUserDefaults alloc] initWithSuiteName:AppGroupId];// 共享的userDefault |
注意线程问题
“Upload”中开始各种方法被调用的线程都不是主线程,所以要注意线程同步问题,并且实测如果强行回调到主线做一些耗时操作,会使”Upload”崩溃,所以非不得已(“Upload”也没有需要处理的UI事件),尽量使用当前线程,并且在开始和结束的方法中加同步锁. 以及如果开启NSTimer的话,由于不是主线程,记得开启当前线程的Runloop!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
- (void)setupTimerThread { if (self.tiemrThread.executing) { return; } self.tiemrThread = [[NSThread alloc] initWithTarget:self selector:@selector(startTimer) object:nil]; self.tiemrThread.name = @"timerThread"; [self.tiemrThread start]; } - (void)startTimer { @autoreleasepool { [self postPushStatus:ANPushHeartbeatStatusPushing]; self.timer = [NSTimer timerWithTimeInterval:15 repeats:YES block:^(NSTimer * _Nonnull timer) { }]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } } - (void)stopTimer { [self performSelector:@selector(invalidateTimer) onThread:self.tiemrThread withObject:nil waitUntilDone:NO]; } - (void)invalidateTimer { if (self.timer) { [self.timer invalidate]; self.timer = nil; } } |
以上是一些开发过程中学习到知识与坑点,如有问题,欢迎交流探讨.
参考链接
Live Screen Broadcast with ReplayKit