本篇文章仅针对iOS10+系统,如果需要支持iOS9请参考下面链接
因为10+部分涉及知识面较多,所以这篇文章主要围绕录屏,其他涉及到的我会另开篇幅。
将涉及其它知识面:
1.App Extension
2.App Group Share
3.H264与CMSampleBufferRef结构分析
4.VideoToolBox硬编码
录屏整体流程如下:
1.触发录屏
2.准备工作
3.开始录屏
4.处理数据流保存到指定文件
5.结束录制
6.Group共享到主App中(或者直接对此文件进行操作)
进入正题:
苹果在iOS9已经支持了屏幕录制,但相比较安卓的来说开发者的可操作性又少又差。在iOS10之后,ReplayKit又开放了一系列的API,给了开发者更多的操作空间,但从实际体验来说友好性并不理想。
iOS10之后的一些API更多地偏向于流数据的处理,通过App Extension的配合完成屏幕数据流或者其他数据流的采集,通过对数据流的采集和处理我们能做很多事情,比如进行推流直播或者编码保存信息,所以屏幕录制其实只是在此基础上延伸出了一个使用方式,而且我认为10-12之间使用App Extension的方式并不是一个好的录屏解决方案,相比iOS9他的流程更加繁琐和不可控,在使用方向上个人觉得更适合于直播的方向。
iOS10和iOS11屏幕录制
把这两个系统版本放在了一起是因为他们非常的相近。
1.创建App Extension
选择File-New-Target,选择如下
下一步中勾选一下UI(不勾选也是可以的,勾选的话方便我们进行宿主App的验证)
创建好之后我们会发现工程中多了一些东西:
上面的文件夹包含的是主要的功能----录制状态的变化和数据流的获取都在这里面,下面的文件夹包含的主要是从选中Sheet的Item到开始录制中的一个过渡VC,在这个VC中你可以加入一些账号验证或者其他想要做的事情,对整体功能来说可有可无。
2.选择App Extension
在几年前,跨App的直播在苹果上是不存在,想做游戏直播一般都是电脑或者安卓机。玩过直播App的同学可能留意到,市面上的一些做直播的App很多已经提供了直播Extension功能,即只使用其App的一个Extension功能进行跨App的游戏直播。
像下面这样这些App都提供了这个功能:
其实在操作完第一步之后,我们的App也具备展示在Sheet中的能力,下面我们来布局代码。
iOS10中在需要触发录屏按钮的地方触发这个方法:
- (void)startREC_showExtension {
[RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithHandler:^(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error) {
if (broadcastActivityViewController) {
broadcastActivityViewController.delegate = self;
broadcastActivityViewController.modalPresentationStyle = UIModalPresentationPopover;
[self presentViewController:broadcastActivityViewController animated:YES completion:nil];
}
}];
}
点击之后效果如下:
这个方法将加载当前支持Broadcast Upload Extension的扩展,RPBroadcastActivityViewController是弹出的Sheet,设置好代理之后之后我们选中了某个Item就会进入到过度的一个VC中,就是上面图中的BroadcastSetupViewController。
在这个VC中我们可以进行宿主App的校验或者账号信息的登陆(因为所有的需要直播的App都能调出我们的这个Extension)。
iOS11中我们可以直接启动对应的item跳过选择这一步:
+ (void)loadBroadcastActivityViewControllerWithPreferredExtension:(NSString * _Nullable)preferredExtension handler:(nonnull void(^)(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error))handler API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvos);
上面这个方法中preferredExtension参数指的是我们创建的BroadcastVCExtension中Info.plist的bundleID,你也可以从工程----Target----BroadcastSetupUI(名称可能不一样)中找到这个ID。
我们我们操作停当之后需要触发userDidFinishSetup这个方法才能回调下一步,可以看下这个.m文件中userDidFinishSetup这个方法:
iOS10
- (void)completeRequestWithBroadcastURL:(NSURL *)broadcastURL broadcastConfiguration:(RPBroadcastConfiguration *)broadcastConfiguration setupInfo:(nullable NSDictionary <NSString *, NSObject <NSCoding> *> *)setupInfo API_DEPRECATED("No longer supported", ios(10.0,11.0),tvos(10.0,11.0));
iOS11
- (void)completeRequestWithBroadcastURL:(NSURL *)broadcastURL setupInfo:(nullable NSDictionary <NSString *, NSObject <NSCoding> *> *)setupInfo API_AVAILABLE(ios(11.0),tvos(11.0));
上面两个一样的作用,表示设置完成,userInfo可以传递设置的参数,方法完成后触发RPBroadcastActivityViewController的代理如下:
#pragma mark RPBroadcastActivityViewControllerDelegate
- (void)broadcastActivityViewController:(RPBroadcastActivityViewController *)broadcastActivityViewController didFinishWithBroadcastController:(nullable RPBroadcastController *)broadcastController error:(nullable NSError *)error {
dispatch_async(dispatch_get_main_queue(), ^{
[broadcastActivityViewController dismissViewControllerAnimated:YES completion:nil];
});
self.broadcastController = broadcastController;
[broadcastController startBroadcastWithHandler:^(NSError * _Nullable error) {
if (!error) {
NSLog(@"启动");
} else {
NSLog(@"error: %@", error);
}
}];
}
到此为止,我们已经完成了从调出Sheet到触发屏幕录制这个阶段,下面进行屏幕录制数据流的处理。
请注意,在Extension文件中操作的调试需要选择对应的Target,因为他是工程中一个独立的Target,否则无法调试!!!
选择Target----选择宿主App----进行调试
如下图:
以上方法在录屏的时候触发的录屏是在应用内,比如你下拉一下通知栏就会自动暂停,再次进入App内提示会自动提示你是否继续直播屏幕,而iOS11触发的录屏可以是在App外,进行一个跨App的屏幕录制,所以iOS能进行跨App的录屏是在iOS11之后。
iOS11录屏要想在应用外使用下面的方法触发:
首先将录屏功能加到控制面板里:设置-控制中心-屏幕录制加上
然后上拉或下划调出控制中心,长按录制按钮,调出录屏控制面板,选择对应直播功能的Extension,如下图:
iOS11有一组专门对屏幕录制的API,这组API和iOS9上的一组非常相似,不同的是数据是以流的形式返回,同样的,这组API只能对App内进行屏幕录制,无法跨App进行录制(任何使App挂起的操作都会打断屏幕录制),好处是避开了Extension,简化了操作。
- (void)startCaptureWithHandler:(nullable void(^)(CMSampleBufferRef sampleBuffer, RPSampleBufferType bufferType, NSError * _Nullable error))captureHandler completionHandler:(nullable void(^)(NSError * _Nullable error))completionHandler API_AVAILABLE(ios(11.0), tvos(11.0));
- (void)stopCaptureWithHandler:(nullable void(^)(NSError * _Nullable error))handler API_AVAILABLE(ios(11.0), tvos(11.0));
两个方法一个是开始,另一个是结束,开始的方法中回调了屏幕的数据流,我们需要对这组数据流进行编码操作,推流或者保存。如果你只是要进行App内屏幕录制,这组方法无疑是非常好的。
3.流程控制和数据流的采集处理
我们切到第一个文件夹中的SampleHandle.m文件中,可以看到.m中的方法分为两部分,一部分是对录制流程的控制另一部分是数据流的采集。
流程控制这部分代码:
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
NSLog(@"1");
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
}
上面这个方法是对过度VC中参数的传递,我们可以在这个方法中拿到过度页面的传值。
- (void)broadcastPaused {
NSLog(@"2");
// User has requested to pause the broadcast. Samples will stop being delivered.
}
- (void)broadcastResumed {
NSLog(@"3");
// User has requested to resume the broadcast. Samples delivery will resume.
}
- (void)broadcastFinished {
NSLog(@"4");
// 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;
}
}
我们在这个方法里拿到数据流,这里的数据流是未编码的,通常情况下如果要做推流或者文件保存,我们会将其进行一个H264的编码。
需要注意的是这个方法的回调是有限制的,仅当你的屏幕发生了实质性的变化才会触发,如果你的屏幕一直静止则不会触发,在调试的时候需要多注意。
数据流的硬编码使用VideoToolBox,此处不再过多叙述会另开篇幅。
拿到编码后的流数据我们可以做一些推流或者文件写入的操作。
4.停止录屏
在上述App内屏幕录制时想要主动停止需要broadcastController调用下面这个方法
- (void)finishBroadcastWithHandler:(void(^)(NSError * _Nullable error))handler;
(由于App内录制的特殊性,当你的App切换到后台被挂起时会自动暂停录制,当你激活App时会收到是否继续直播的提示,当你选择否时也会停止录屏。)
录屏停止时在SampleHandler.m中会回调下面的方法:
- (void)broadcastFinished {
NSLog(@"STOP");
// User has requested to finish the broadcast.
}
我们在这个方法中可以整理数据和进行收尾工作。
当你使用的是跨App的方式进行录制的时候,想要主动结束就需要用户手动点击关闭控制面板的录屏按钮(暂时没找到如何在程序中主动关闭),在SampleHandler的实例方法中有一个下面的方法,似乎可以主动关闭录屏(但我测试的时候却无法真正关闭),而且我不知道如何通知到这个Extension去关闭录屏(有同学提议用进程间通知的方式暂时还没尝试),这里Mark一下。
- (void)finishBroadcastWithError:(NSError *)error;
5.录屏文件数据的共享
每个Extension都需要一个宿主App,并且有自己的沙盒,当我们把录屏文件保存到沙盒中时宿主App是无法获取到的,那么只有采用共享的方式才能让宿主App拿到录屏文件。
App Group Share帮我们解决了这个问题,通过设置组间共享的模式,使得同一个Group下面的App可以共享资源,解决了沙盒的限制。
到此为止,基于iOS10和11的录屏操作已经完整告一段落,从中我们就可以看出,想用iOS10和11的API去进行录屏操作不但反用户操作行为而且反开发行为,冗长的流程和各种限制使得录屏体验非常差,所以市面上使用这套API的大部分App只做直播方向。
iOS12屏幕录制
到了iOS12,苹果又开放了一部分API,这使得录屏变得可行并且易于操作。
给予我们极大帮助的是这个类
RPSystemBroadcastPickerView
点击去看:
@interface RPSystemBroadcastPickerView : UIView <NSCoding>
/* @abstract Bundle identifier of extension that should be used for broadcast. Default is nil which means that all extensions will be presented */
@property (nonatomic, strong, nullable) NSString *preferredExtension;
/* @abstract Indicates whether the Microphone button is visible in broadcast picker view. Default is YES. */
@property (nonatomic, assign) BOOL showsMicrophoneButton;
@end
RPSystemBroadcastPickerView继承与UIView只有两个属性,第一个表示要启动的Extension标识,nil则全部弹出供选择,第二个标识是否显示麦克风按钮,如果显示并且选择则会把声音一同录入。
_broadPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
_broadPickerView.preferredExtension = nil;
[self.view addSubview:_broadPickerView];
请注意,这里有一点不同,当你preferredExtension=nil时你需要选择要用的直播Extension,选择好之后进入了10和11对流处理的模式;当你指定了preferredExtension=@“XXX”的时候,直接开始录制,开发者不进行流数据处理,录制完成之后自动保存到相册。
对于单纯的录屏来说,指定preferredExtension无意是最便捷的方式,虽然API并没有暴露录屏开始和结束的回调方法,但我们可以将RPSystemBroadcastPickerView进行一个响应事件的传递从我们自定义的View中传递给RPSystemBroadcastPickerView。当然严格地来说这样并不能精确地判断,还需要加入一些逻辑上的检测。例如:
[[RPScreenRecorder sharedRecorder] isRecording];
最后总结一下,如果需要的只是App内录制,那么iOS9的API、iOS11的不包含Extension的API、iOS12的RPSystemBroadcastPickerView是比较好的选择;如果你需要跨App的录屏,那么建议尽量从iOS12开始支持;而Extension这种,更多的是为了直播而进行的服务。