基于ReplayKit实现屏幕录制

前言

近期项目中需要完成一个实现屏幕录制(包含画面、麦克风、app内声音)功能,并压缩上传服务器,因此对iOS系统的replaykit进行了初步的研究,现分享一下结果:

截屏2021-06-25 下午11.08.22.png

概述

基于目前项目的快速迭代要求,首先想到的是官方ReplayKit框架,初步调研发现ReplayKit框架最低要求是iOS9.0,且支持屏幕、麦克风、app声音的录制,满足技术可行性,因此决定直接采用ReplayKit实施。

ReplayKit介绍

ReplayKit在WWDC15的时候随iOS9.0推出。当时的目的是给游戏开发者录制玩游戏的视频,进行社交分享使用。 除了录制和共享外,ReplayKit还包括一个功能齐全的用户界面,玩家可以用来编辑其视频剪辑。

Replaykit功能介绍视频 WWDC15

ReplayKit除了实现屏幕录制以外,还能够将录制的音视频流实时广播出去,对于iOS端,需要两个关键技术:屏幕内容采集和媒体流广播。前者需要系统提供相关权限,可以让开发者采集到app或者整个系统层面的屏幕上的内容,后者需要系统提供采集到实时的视频流和音频流,这样才能通过推流到服务器,实现媒体流的广播。

录制

iOS9.0

//头文件
#import 

//启动录制
- (void)startRecordingWithMicrophoneEnabled:(BOOL)microphoneEnabled handler:(nullable void (^)(NSError *_Nullable error))handler API_DEPRECATED("Use microphoneEnabled property", ios(9.0, 10.0)) API_UNAVAILABLE(macOS);

//停止录制
- (void)stopRecordingWithHandler:(nullable void (^)(RPPreviewViewController *_Nullable previewViewController, NSError *_Nullable error))handler;

通过stopRecordingWithHandler的api,回调previewViewController(预览页面),通过presentViewController推出预览页,可以:裁剪、分享、保存相册

[self presentViewController:previewViewController animated:YES completion:^{}];

预览页监听操作结果

#pragma mrak - RPPreviewViewControllerDelegate

- (void)previewController:(RPPreviewViewController *)previewController didFinishWithActivityTypes:(NSSet  *)activityTypes
{
    if ([activityTypes containsObject:@"com.apple.UIKit.activity.SaveToCameraRoll"]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"保存成功");
        });
    }
    if ([activityTypes containsObject:@"com.apple.UIKit.activity.CopyToPasteboard"]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"复制成功");

        });
    }
}

- (void)previewControllerDidFinish:(RPPreviewViewController *)previewController
{
    [previewController dismissViewControllerAnimated:YES completion:^{
        
    }];
}

通过拦截RPPreViewController,打印录制视频的地址:videoUrl = file:///private/var/mobile/Library/ReplayKit/ReplaykitDemo_06-28-2021%2015-51-13_1.mp4 可以发现文件存在于系统的位置,所以无法直接获取

总结

优点:
高度封装,操作简单,能够快速的实现屏幕录制功能。

缺点:

  1. 不能获取到视频录制时的数据,只能在停止录制视频的时候获取到苹果已经处理合成好的MP4文件
  2. 不能直接获取录制好的视频文件,需要先通过用户存储到相册,你才能通过相册去访问到该文件、
  3. 停止录制的时候需要弹出一个视频的预览窗口,你可以在这个窗口进行保存或者取消或者分享该视频文件、你还可以直接编辑该视频
  4. 由于上面的限制,你只能在用户存储录制的视频保存到相册你才能访问。想要上传该视频到服务器,你还需要把相册的那个视频先想办法copy到沙盒中,然后再开始上传服务器。
  5. 无法配置屏幕录制参数

iOS10.0

优化内容:

//新增启动录制
- (void)startRecordingWithHandler:(nullable void (^)(NSError *_Nullable error))handler API_AVAILABLE(ios(10.0), tvos(10.0), macos(11.0));

//通过microphoneEnabled 控制是否开启麦克风
@property (nonatomic, getter = isMicrophoneEnabled) BOOL microphoneEnabled API_UNAVAILABLE(tvOS);

//结束录制以及录制完成后跳转预览页做编辑操作同iOS9.0保持一致

总结:同iOS9.0

新增内容

iOS 10 系统在 iOS 9 系统的 ReplayKit保存录屏视频的基础上,增加了视频流实时直播功能(streaming live),可以将广播出来的直播流进行分发和直播。具体实现是通过增加ReplayKit的扩展分别为Broadcast Upload Extension 和 Broadcast Setup UI Extension,
Broadcast Upload Extension 是处理捕捉到App屏幕录制的数据的
Broadcast Setup UI Extension一些关于屏幕捕捉的UI交互

步骤:

  1. 添加扩展插件file->new->target->Broadcast upload Extension
    系统会生成两个target,两个对应的目录以及4个文件分别:
  • SampleHandler.h
  • SampleHandler.m
  • BroadcastSetupViewController.h
  • BroadcastSetupViewController.m

SampleHandler主要处理流数据RPSampleBufferTypeVideo、RPSampleBufferTypeAudioApp、RPSampleBufferTypeAudioMicBroadcastSetupViewController作为启动进程间插入的交互页面,可以用于用户输入信息鉴权,或者自定义其他界面

  1. 启动备选界面
//启动备选界面
+ (void)loadBroadcastActivityViewControllerWithHandler:(void (^)(RPBroadcastActivityViewController *_Nullable broadcastActivityViewController, NSError *_Nullable error))handler;

[RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithHandler:^(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error) {
    if (error) {
        NSLog(@"RPBroadcast err %@", [error localizedDescription]);
    }
    broadcastActivityViewController.delegate = self;
    [self presentViewController:broadcastActivityViewController animated:YES completion:nil];
}];

  1. 通过代理回调,启动录制进程
#pragma mark - Broadcasting

- (void)broadcastActivityViewController:(RPBroadcastActivityViewController *) broadcastActivityViewController
       didFinishWithBroadcastController:(RPBroadcastController *)broadcastController
                                  error:(NSError *)error {
    
    [broadcastActivityViewController dismissViewControllerAnimated:YES completion:nil];
                                                        
    self.broadcastController = broadcastController;
    self.broadcastController.delegate = self;
    if (error) {
        return;
    }

    //启动广播
    [broadcastController startBroadcastWithHandler:^(NSError * _Nullable error) {
        if (!error) {
            NSLog(@"-----start success----");
            // 这里可以添加camerPreview
        } else {
            NSLog(@"startBroadcast:%@",error.localizedDescription);
        }
    }];
}
  1. UI交互配置
- (void)userDidFinishSetup {
    NSURL *broadcastURL = [NSURL URLWithString:@"http://apple.com/broadcast/streamID"];
    NSDictionary *setupInfo = @{ @"broadcastName" : @"example" };
    // Tell ReplayKit that the extension is finished setting up and can begin broadcasting
    [self.extensionContext completeRequestWithBroadcastURL:broadcastURL setupInfo:setupInfo];
}

- (void)userDidCancelSetup {
    [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"YourAppDomain" code:-1 userInfo:nil]];
}

- (void)viewWillAppear:(BOOL)animated
{
    [self userDidFinishSetup];
}
  1. 数据流的接收与处理
- (void)broadcastStartedWithSetupInfo:(NSDictionary *)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;
    }
}

processSampleBuffer方法就是最终采集到的音频、视频原始数据。其中音频未做混音,包括麦克音频pcm和app音频pcm,而视频输出为yuv数据。

总结

优点:

  1. 除了录屏以外,新增直播特性,功能更加强大
  2. 能够拿到音视频原始流数据,满足一些需要做音视频特效的需求

缺点:

  1. 增加用户交互成本,需要拉起录制列表,然后用户点击选择对应的录制程序,操作成功相对高一些
  2. 集成难度相比于iOS9.0加大,处理原始数据难度比较大

iOS11.0

新增内容

新增api,跳过iOS10的中间列表sheet在点击选择的过程,但是还是只能录制app内的内容。

+ (void)loadBroadcastActivityViewControllerWithPreferredExtension:(NSString * _Nullable)preferredExtension handler:(nonnull void(^)(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error))handler API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(tvOS);

处理的流程同iOS10的扩展插件

新增开启屏幕捕捉

开启捕捉回调sampleBuffer
- (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), macos(11.0));

可以直接调用接口捕捉到sampleBuffer,省去了iOS10的扩展插件环节,可以直接拿到想要的buffer裸数据,无需中间交互环节,完成满足最上面所说的项目要求

总结:

优点:

  1. 调用方法简单,易于集成
  2. 无中间用户交互环节,用户交互成本低
  3. 直接获取到音视频裸数据

缺点:
裸数据处理难度稍大

补充

音视频裸数据编码合成mp4写入本地沙盒

  1. iOS端编码合成采用AVAssetWriter,配套AVAssetWriterInput使用
//writer
@property (nonatomic, strong) AVAssetWriter *assetWriter;
//视频输入
@property (nonatomic, strong) AVAssetWriterInput *assetWriterVideoInput;
//音频输入
@property (nonatomic, strong) AVAssetWriterInput *assetWriterAudioInput;
//app内音频输入
@property (nonatomic, strong) AVAssetWriterInput *assetWriterAppAudioInput;

//初始化
self.assetWriter = [AVAssetWriter assetWriterWithURL:[NSURL fileURLWithPath:videoOutPath] fileType:AVFileTypeMPEG4 error:&error];

2.视频编码配置

    //视频的配置
    NSDictionary *compressionProperties = @{
        AVVideoProfileLevelKey : AVVideoProfileLevelH264HighAutoLevel,
        AVVideoH264EntropyModeKey      : AVVideoH264EntropyModeCABAC,
        AVVideoAverageBitRateKey       : @(DEVICE_WIDTH * DEVICE_HEIGHT * 6.0),
        AVVideoMaxKeyFrameIntervalKey  : @15,
        AVVideoExpectedSourceFrameRateKey : @(15),
        AVVideoAllowFrameReorderingKey : @NO};
        
    NSNumber* width= [NSNumber numberWithFloat:DEVICE_WIDTH];
    NSNumber* height = [NSNumber numberWithFloat:DEVICE_HEIGHT];

    NSDictionary *videoSettings = @{
            AVVideoCompressionPropertiesKey :compressionProperties,
            AVVideoCodecKey :AVVideoCodecTypeH264,
            AVVideoWidthKey : width,
            AVVideoHeightKey: height
    };

    self.assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];

3.音频编码配置

    // 音频设置
    NSDictionary * audioCompressionSettings = @{                       AVEncoderBitRatePerChannelKey : @(28000),
        AVFormatIDKey : @(kAudioFormatMPEG4AAC),
        AVNumberOfChannelsKey : @(1),
        AVSampleRateKey : @(22050) };

    self.assetWriterAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioCompressionSettings];

4.input添加writer

    //视频
    [self.assetWriter addInput:self.assetWriterVideoInput];
    [self.assetWriterVideoInput setMediaTimeScale:60];
    [self.assetWriterVideoInput setExpectsMediaDataInRealTime:YES];
        
    [self.assetWriter setMovieTimeScale:60];
        
    //音频
    [self.assetWriter addInput:self.assetWriterAudioInput];
    self.assetWriterAudioInput.expectsMediaDataInRealTime = YES;
    
    //app内声音
    [self.assetWriter addInput:self.assetWriterAppAudioInput];
    self.assetWriterAppAudioInput.expectsMediaDataInRealTime = YES;

5.合并代码

    [[RPScreenRecorder sharedRecorder] startCaptureWithHandler:^(CMSampleBufferRef  _Nonnull sampleBuffer, RPSampleBufferType bufferType, NSError * _Nullable error) {
            
        if (CMSampleBufferDataIsReady(sampleBuffer)) {
                
            if (self.assetWriter.status == AVAssetWriterStatusUnknown && bufferType == RPSampleBufferTypeVideo) {
                    [self.assetWriter startWriting];
                    [self.assetWriter startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
                }

            if (self.assetWriter.status == AVAssetWriterStatusFailed) {
                    NSLog(@"An error occured.");
                    [self writeDidOccureError:self.assetWriter.error callBack:handler];
                    return;
                }
            
                            if (bufferType == RPSampleBufferTypeVideo) {
                    if (self.assetWriterVideoInput.isReadyForMoreMediaData) {
                        [self.assetWriterVideoInput appendSampleBuffer:sampleBuffer];
                    }
                }else if (bufferType == RPSampleBufferTypeAudioMic)
                {
                    if (self.assetWriterAudioInput.isReadyForMoreMediaData) {
                        [self.assetWriterAudioInput appendSampleBuffer:sampleBuffer];
                        [self sampleBuffer2PcmData:sampleBuffer];
                    }
                }else if (bufferType == RPSampleBufferTypeAudioApp)
                {
                    if (self.assetWriterAppAudioInput.isReadyForMoreMediaData) {
                        [self.assetWriterAppAudioInput appendSampleBuffer:sampleBuffer];
                    }
                } 
            
        } completionHandler:^(NSError * _Nullable error) {
            if (!error) {
                // Start recording
                NSLog(@"Recording started successfully.");
                
            }else{
                //show alert
            }
        }];

音频解码获取声音大小

关键的代码

/// buffer转pcm
/// @param audiobuffer
- (void)sampleBuffer2PcmData:(CMSampleBufferRef)audiobuffer
{
    CMSampleBufferRef ref = audiobuffer;
    if(ref==NULL){
        return;
    }
    
    //copy data to file
    //read next one
    AudioBufferList audioBufferList;
    NSMutableData *data=[[NSMutableData alloc] init];
    CMBlockBufferRef blockBuffer;
    
    CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(ref, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, 0, &blockBuffer);
 
    for( int y=0; y

总结

通过以上各个系统版本的对比,最终项目采用了iOS11的startCaptureWithHandler接口实现屏幕录制数据采集,然后通过AVAssetWriter进行编码合成mp4文件以及通过音频裸数据提取声音,最终完成该需求。以上均为代码的片段,还需要集合业务考虑各种异常情况的处理,以及视频、音频的编码配置需要进一步研究,通过优化配置参数,能够进一步提升录制视频的体验,整个过程坑点有点进一步补充。

你可能感兴趣的:(基于ReplayKit实现屏幕录制)