iOS录屏

iOS录屏功能与系统版本关联,每个版本使用的库或者API不尽相同,实现的功能也不太一样,具体如下:

1、iOS 9以前
iOS 9以前系统未开放相关的API,需要使用私有库开发相关功能,目前框兼容于iOS 9+系统版本,且苹果已将该私有库彻底禁用,无法使用,故忽略。
2、iOS 9

iOS9开始,苹果新增了 ReplayKit 框架,使用该框架中的API进行录屏,该功能只能录制应用内屏幕,且无法操作视频/音频流,最终只能在预览页面进行“保存”、“拷贝”、“分享”等操作。具体使用如下:

// 判断是否正在录屏
if ([RPScreenRecorder sharedRecorder].isRecording) {
        NSLog(@"正在录制");
        return;
    }
// 判断录屏功能是否可用
if (![[RPScreenRecorder sharedRecorder] isAvailable]) {
        NSLog(@"录屏功能不可用");
    }
// 开始录制
[[RPScreenRecorder sharedRecorder] startRecordingWithMicrophoneEnabled:YES handler:^(NSError * _Nullable error) {
                if (error) {
                    NSLog(@"开始录制出错:%@", error);
                } else {
                    NSLog(@"开始录制");
                }
            }];
[[RPScreenRecorder sharedRecorder] stopRecordingWithHandler:^(RPPreviewViewController * _Nullable previewViewController, NSError * _Nullable error) {
        if (error) {
            NSLog(@"结束录屏失败: %@", error);
            return ;
        }
        
        previewViewController.previewControllerDelegate = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [self presentViewController:previewViewController animated:YES completion:nil];
        });
    }];

#pragma mark - RPPreviewViewControllerDelegate
- (void)previewController:(RPPreviewViewController *)previewController didFinishWithActivityTypes:(NSSet <NSString *> *)activityTypes {
    if ([activityTypes containsObject:UIActivityTypeSaveToCameraRoll]) {
        // 保存到相册
    }
    
    [previewController dismissViewControllerAnimated:YES completion:nil];
}
3、iOS 10

从iOS 10开始,苹果新增了录制系统屏幕的API,即应用即使退出前台也能持续录制,以下称为“系统屏幕录制”,区分于“应用屏幕录制”。

  • 应用屏幕录制
    与iOS 9的API类似,只不过将iOS 9中开始录屏的方法 startRecordingWithMicrophoneEnabled 参数改为使用属性,
[RPScreenRecorder sharedRecorder].microphoneEnabled = YES;
                [[RPScreenRecorder sharedRecorder] startRecordingWithHandler:^(NSError *error){
                    if (error) {
                        NSLog(@"开始录制error %@", error);
                    } else {
                        NSLog(@"开始录制");
                    }
                }];

其他与iOS 9一样

  • 系统屏幕录制
    该功能同样基于 ReplayKit 实现,只不过具体实现并不是在主应用中,而是在主应用工程的Extension(扩展)中实现,具体为 Broadcast Setup UI Extension 和Broadcast Upload Extension,其中Broadcast Setup UI Extension负责UI绘制,Broadcast Upload Extension负责数据处理。
    大体流程如下:
    主应用 唤起 已实现 BBroadcast Setup UI Extension 应用的对应扩展,会在主应用中显示该扩展的UI页面,在该页面调用 “userDidFinishSetup” 启动 “Broadcast Upload Extension” 开始进行屏幕录制,并在该扩展中处理视频流、音频流数据。

Broadcast Setup UI Extension :

// Call this method when the user has finished interacting with the view controller and a broadcast stream can start
- (void)userDidFinishSetup {
    
    // URL of the resource where broadcast can be viewed that will be returned to the application
    NSURL *broadcastURL = [NSURL URLWithString:@"http://apple.com/broadcast/streamID"];
    
    // Dictionary with setup information that will be provided to broadcast extension when broadcast is started
    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 {
    // Tell ReplayKit that the extension was cancelled by the user
    [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"YourAppDomain" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"StopBroadcast"}]];
}

Broadcast Upload Extension

- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
    // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
    // 用户已启动广播。
    // 可以提供来自UI扩展的设置信息,但这是可选的
    
    NSLog(@"启动广播");
}

- (void)broadcastPaused {
    // User has requested to pause the broadcast. Samples will stop being delivered.
    // 用户暂停广播。
    
    NSLog(@"暂停广播");
}

- (void)broadcastResumed {
    // User has requested to resume the broadcast. Samples delivery will resume.
    // 用户恢复广播
    
    NSLog(@"恢复广播");
}

- (void)broadcastFinished {
    // User has requested to finish the broadcast.
    // 用户完成广播
    
    NSLog(@"完成广播");
}

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    switch (sampleBufferType) {
        case RPSampleBufferTypeVideo:
            // Handle video sample buffer
            // 视频流
            // 得到YUV数据
            NSLog(@"视频流");
            
            break;
        case RPSampleBufferTypeAudioApp:
            // Handle audio sample buffer for app audio
            // 处理app音频
            NSLog(@"App音频流");
            
            break;
        case RPSampleBufferTypeAudioMic:
            // Handle audio sample buffer for mic audio
            // 处理麦克风音频
            NSLog(@"麦克风音频流");
            
            break;
            
        default:
            break;
    }
}

唤起录屏功能(在另外的应用,区分主应用):

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

// 用户选了扩展之后回调的方法, 在这方法里正式调起UI扩展
- (void)broadcastActivityViewController:(RPBroadcastActivityViewController *)broadcastActivityViewController
       didFinishWithBroadcastController:(RPBroadcastController *)broadcastController
                                  error:(NSError *)error {
    dispatch_async(dispatch_get_main_queue(), ^{
        [broadcastActivityViewController dismissViewControllerAnimated:YES completion:nil];
    });
    
    NSLog(@"BundleID %@", broadcastController.broadcastExtensionBundleID);
    self.broadcastController = broadcastController;
    if (error) {
        NSLog(@"BAC: %@ didFinishWBC: %@, err: %@",
              broadcastActivityViewController,
              broadcastController,
              error);
        return;
    }
    [broadcastController startBroadcastWithHandler:^(NSError * _Nullable error) {
        if (!error) {
            NSLog(@"--fshfka----success");
        }
        else {
            NSLog(@"startBroadcast %@",error.localizedDescription);
        }
    }];
}

// 用来接收直播传回来的NSDictionary
- (void)broadcastController:(RPBroadcastController *)broadcastController
       didUpdateServiceInfo:(NSDictionary <NSString *, NSObject <NSCoding> *> *)serviceInfo {
    NSLog(@"didUpdateServiceInfo: %@", serviceInfo);
}

// 直播关闭
- (void)broadcastController:(RPBroadcastController *)broadcastController
         didFinishWithError:(NSError *)error {
    NSLog(@"didFinishWithError: %@", error);
}

4、iOS 11

iOS 11官方开放了应用内录屏的流数据处理API,即可直接操作视频流、音频流,而不是只能预览、保存、分享。

  • 应用屏幕录制
- (NSString *)videoPath {
    if (!_videoPath) {
        NSArray *pathDocuments = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *outputURL = pathDocuments[0];
        uint32_t random = arc4random() % 1000;
        _videoPath = [[outputURL stringByAppendingPathComponent:[NSString stringWithFormat:@"%u", random]] stringByAppendingPathExtension:@"mp4"];
        self.compressVideoPath = [[outputURL stringByAppendingPathComponent:[NSString stringWithFormat:@"%u_compress", random]] stringByAppendingPathExtension:@"mp4"];
        NSLog(@"%@", _videoPath);
        NSLog(@"%@", self.compressVideoPath);
    }
    return _videoPath;
}

- (AVAssetWriter *)assetWriter {
    if (!_assetWriter) {
        NSError *error = nil;
        _assetWriter = [AVAssetWriter assetWriterWithURL:[NSURL fileURLWithPath:self.videoPath]
                                                fileType:AVFileTypeMPEG4
                                                   error:&error];
        
        if (error) {
            NSLog(@"初始化 AVAssetWriter 失败:%@", error);
        }
        
        if ([_assetWriter canAddInput:self.assetWriterInput]) {
            [_assetWriter addInput:self.assetWriterInput];
        }
    }
    return _assetWriter;
}

- (AVAssetWriterInput *)assetWriterInput {
    if (!_assetWriterInput) {
        NSDictionary *compressionProperties = @{
                                                AVVideoAverageBitRateKey : [NSNumber numberWithDouble:2000 * 1000]
                                                };
        
        NSDictionary *videoSettings = @{
                                        AVVideoCompressionPropertiesKey : compressionProperties,
                                        AVVideoCodecKey                 : AVVideoCodecH264,
                                        AVVideoWidthKey                 : [NSNumber numberWithFloat:self.view.frame.size.width],
                                        AVVideoHeightKey                : [NSNumber numberWithFloat:self.view.frame.size.height]
                                        };
        
        _assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
        _assetWriterInput.expectsMediaDataInRealTime = YES;
    }
    return _assetWriterInput;
}

// 开始录屏
[[RPScreenRecorder sharedRecorder] startCaptureWithHandler:^(CMSampleBufferRef  _Nonnull sampleBuffer, RPSampleBufferType bufferType, NSError * _Nullable error) {
                if (self.assetWriter.status == AVAssetWriterStatusUnknown && bufferType == RPSampleBufferTypeVideo) {
                    [self.assetWriter startWriting];
                    CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
                    int64_t videopts  = CMTimeGetSeconds(pts) * 1000;
                    
                    // 丢掉无用帧
                    if (videopts < 0) {
                        NSLog(@"无用帧");
                        return ;
                    }
                    
                    [self.assetWriter startSessionAtSourceTime:pts];
                }
                
                if (self.assetWriter.status == AVAssetWriterStatusFailed) {
                    NSLog(@"An error occured: %@", self.assetWriter.error);
                    [self stopRecord:nil];
                    return;
                }
                
                if (bufferType == RPSampleBufferTypeVideo) {
                    if (self.assetWriterInput.isReadyForMoreMediaData) {
                        CFRetain(sampleBuffer);
                        // 将sampleBuffer添加进视频输入源
                        [self.assetWriterInput appendSampleBuffer:sampleBuffer];
                        CFRelease(sampleBuffer);
                    } else {
                        NSLog(@"Not ready for video");
                    }
                }
            } completionHandler:^(NSError * _Nullable error) {
                if (error) {
                    NSLog(@"开始录制error %@",error);
                } else {
                    NSLog(@"开始录制");
                }
            }];

// 结束录屏
[[RPScreenRecorder sharedRecorder] stopCaptureWithHandler:^(NSError * _Nullable error) {
            if (error) {
                NSLog(@"stopCaptureWithHandler: %@", error);
            }
            // 结束写入
            __weak typeof(self) weakSelf = self;
            [self.assetWriter finishWritingWithCompletionHandler:^{
                // 结束录屏
                {
                    self.assetWriter = nil;
                    self.assetWriterInput = nil;
                }
                __strong typeof(self) strongSelf = weakSelf;
                NSLog(@"屏幕录制结束,视频地址: %@", strongSelf.videoPath);
            }];
        }];

  • 系统屏幕录制
    与iOS 10类似,仅在唤起录屏扩展的API中新增了 PreferredExtension 参数(扩展的BundleID),设置了该参数时,唤起的录屏扩展列表中默认选中该扩展
[RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithPreferredExtension:@"com.asiainfo.LivePlay.UIExtension" handler:^(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error) {
        if (error) {
            NSLog(@"RPBroadcast err %@", [error localizedDescription]);
        } else {
            dispatch_async(dispatch_get_main_queue(), ^{
                broadcastActivityViewController.delegate = self;
                broadcastActivityViewController.modalPresentationStyle = UIModalPresentationPopover;
                [self presentViewController:broadcastActivityViewController animated:YES completion:nil];
            });
        }
    }];
5、iOS 12
  • 应用屏幕录制
    同iOS 11

  • 系统屏幕录制
    可直接在已实现Broadcast Setup UI Extension 和Broadcast Upload Extension扩展的应用中直接使用录屏功能,而不需要在其他应用中唤起。

if (@available(iOS 12, *)) {
        if (!self.broadPickerView) {
            self.broadPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(-100, -100, 100, 100)];
            self.broadPickerView.preferredExtension = @"com.asiainfo.LivePlay.UploadExtension";
            self.broadPickerView.showsMicrophoneButton = YES;
            [self.view addSubview:self.broadPickerView];
        }
    }
    
    for (UIView *subView in self.broadPickerView.subviews) {
        if ([subView isKindOfClass:[UIButton class]]) {
            [(UIButton *)subView sendActionsForControlEvents:UIControlEventTouchDown];
            break;
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容

  • 源起 最近公司应用安全检查,在用户登录页面要防止截屏和录屏导致用户名密码等敏感信息泄露。iOS实现不了不让截屏或者...
    爱笑的猫mi阅读 15,946评论 0 5
  • 最近在做项目安全性方面的工作,需要在APP内敏感页面做防用户截屏录屏的功能,就在网上查阅了一些资料,在这里做个笔记...
    见惯不怪阅读 12,403评论 1 12
  • 1、课程简介 本课程主要讲解,如何在iOS系统的设备上实现,录制屏幕的方案。主要使用iOS系统的Airplay功能...
    杜长生阅读 9,637评论 1 8
  • 录屏 Andorid 手机自带录屏功能 AndroidStudio 中 logcat中有个录屏按钮 ios 手机自...
    HanlyJiang阅读 1,646评论 0 0
  • 闷热过后,雨滴倾泻,滴落着无奈,滴落着孤独。犹如人无奈,心孤独,一生中,总有一份放下,也有一份沧桑,很多事藏的很深...
    风吹流水阅读 302评论 0 5