iOS 屏幕录制实现

录屏API版本变化

  • 主要使用iOS系统的Airplay功能和ReplayKit库实现屏幕录制
  • iOS9开始,苹果新增了 ReplayKit 框架,使用该框架中的API进行录屏,该功能只能录制应用内屏幕,且无法操作视频/音频流,最终只能在预览页面进行“保存”、“拷贝”、“分享”等操作。
  • 从iOS 10开始,苹果新增了录制系统屏幕的API,即应用即使退出前台也能持续录制,以下称为“系统屏幕录制”,区分于“应用屏幕录制”。
  • iOS 11官方开放了应用内录屏的流数据处理API,即可直接操作视频流、音频流,而不是只能预览、保存、分享。
  • 对于录制系统内容,iOS11不允许开发直接调用api来启动系统界别的录制,必须是用户通过手动启动.用户点击进入手机设置页面-> 控制中心-> 自定义 , 找到屏幕录制的功能按钮,将其添加到上方:添加成功
  • 在iOS 12.0+上出现了一个新的UI控件RPSystemBroadcastPickerView,用于展示用户启动系统录屏的指定视图.可以在App界面手动出发录屏

App内部录制屏幕

  • 从App内部录制屏幕,不支持系统界面。只能录制App。
  • 关键类 RPScreenRecorder

录音麦克风声音

  • 首先开启麦克风权限,添加相关配置plist
//
// ViewController
//
//
// Created by song on 2022/01/13.
// Copyright 2022 song. All rights reserved. #import "MainViewController.h"
#import <ReplayKit/ReplayKit.h>
#import <AVFoundation/AVFoundation.h>
#import "SystemScreenRecordController.h" @interface MainViewController ()<RPScreenRecorderDelegate,RPPreviewViewControllerDelegate>
@end @implementation MainViewController -(void)viewDidLoad{
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
[self setupUI];
[self setupScreen];
}
- (void)setupScreen{
AVAuthorizationStatus microPhoneStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
switch (microPhoneStatus) {
case AVAuthorizationStatusDenied:
case AVAuthorizationStatusRestricted:
{
// 被拒绝
[self goMicroPhoneSet];
}
break;
case AVAuthorizationStatusNotDetermined:
{
// 没弹窗
[self requestMicroPhoneAuth];
}
break;
case AVAuthorizationStatusAuthorized:
{
// 有授权
}
break; default:
break;
} }
-(void) goMicroPhoneSet
{
UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"您还没有允许麦克风权限" message:@"去设置一下吧" preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction * cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { }];
UIAlertAction * setAction = [UIAlertAction actionWithTitle:@"去设置" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
dispatch_async(dispatch_get_main_queue(), ^{
NSURL * url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
[UIApplication.sharedApplication openURL:url options:nil completionHandler:^(BOOL success) { }];
});
}]; [alert addAction:cancelAction];
[alert addAction:setAction]; [self presentViewController:alert animated:YES completion:nil];
}
-(void) requestMicroPhoneAuth
{
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { }];
}
- (void)setupUI{
self.title= @"录屏Demo";
self.navigationController.navigationBar.tintColor=[UIColor whiteColor];
self.navigationController.navigationBar.barTintColor = [UIColor greenColor];
self.navigationController.navigationBar.barStyle = UIBarStyleBlack;
[self.navigationController.navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName:[UIColor whiteColor],NSFontAttributeName:[UIFont systemFontOfSize:25]}]; UIBarButtonItem *leftBar = [[UIBarButtonItem alloc ] initWithTitle:@"开始录屏" style:UIBarButtonItemStylePlain target:self action:@selector(start)];
UIBarButtonItem *playBtn = [[UIBarButtonItem alloc] initWithTitle:@"结束录屏" style:UIBarButtonItemStylePlain target:self action:@selector(stop)]; self.navigationItem.rightBarButtonItem = playBtn; self.navigationItem.leftBarButtonItem = leftBar; UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeSystem];
btn1.frame = CGRectMake(110, 100, 100, 33);
btn1.backgroundColor = [UIColor redColor];
[btn1 setTitle:@"点我啊" forState:UIControlStateNormal];
[btn1 addTarget:self action:@selector(systemBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn1];
}
- (void)systemBtnClick{
SystemScreenRecordController *vc = [[SystemScreenRecordController alloc] init];
vc.hidesBottomBarWhenPushed = YES;
[self.navigationController pushViewController:vc animated:YES];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"keyPath:%@,change:%@",keyPath,change);
if ([keyPath isEqualToString:@"available"] && [change[@"new"] integerValue] == 1) {
[self start];
}
}
- (void)checkout{ if (@available(iOS 9.0, *)) {
if ([RPScreenRecorder sharedRecorder].available) {
NSLog(@"可以录屏");
[self start]; }else{
NSLog(@"未授权");
[[RPScreenRecorder sharedRecorder] addObserver:self forKeyPath:@"available" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}
} else {
NSLog(@"不支持录屏");
} }
- (void)start{
if ([RPScreenRecorder sharedRecorder].recording) {
NSLog(@"录制中...");
}else{
NSLog(@"1---[RPScreenRecorder sharedRecorder].microphoneEnabled:%d",[RPScreenRecorder sharedRecorder].microphoneEnabled);
if(![RPScreenRecorder sharedRecorder].microphoneEnabled){
[[RPScreenRecorder sharedRecorder] setMicrophoneEnabled:YES];
}
NSLog(@"2---[RPScreenRecorder sharedRecorder].microphoneEnabled:%d",[RPScreenRecorder sharedRecorder].microphoneEnabled);
[RPScreenRecorder sharedRecorder].delegate = self;
if (@available(iOS 11.0, *)) {
[[RPScreenRecorder sharedRecorder] startCaptureWithHandler:^(CMSampleBufferRef _Nonnull sampleBuffer, RPSampleBufferType bufferType, NSError * _Nullable error) {
NSLog(@"拿到流,可以直播推流");
switch (bufferType) {
case RPSampleBufferTypeAudioApp:
NSLog(@"内部音频流");
break;
case RPSampleBufferTypeVideo:
NSLog(@"内部视频流");
break;
case RPSampleBufferTypeAudioMic:
NSLog(@"麦克风音频");
break;
default:
break;
}
} completionHandler:^(NSError * _Nullable error) {
NSLog(@"startCaptureWithHandler completionHandler");
if (error) { }else{ }
}];
}
else if (@available(iOS 10.0, *)) {
[[RPScreenRecorder sharedRecorder] startRecordingWithHandler:^(NSError * _Nullable error) {
NSLog(@"startRecordingWithHandler:%@",error);
}];
} else if(@available(iOS 9.0, *)) {
[[RPScreenRecorder sharedRecorder] startRecordingWithMicrophoneEnabled:YES handler:^(NSError * _Nullable error) {
NSLog(@"startRecordingWithMicrophoneEnabled:%@",error);
}];
} } }
- (void)stop{
if ([RPScreenRecorder sharedRecorder].recording) {
[[RPScreenRecorder sharedRecorder] stopRecordingWithHandler:^(RPPreviewViewController * _Nullable previewViewController, NSError * _Nullable error) {
NSLog(@"stopRecordingWithHandler");
if (!error) {
previewViewController.previewControllerDelegate = self;
[self presentViewController:previewViewController animated:YES completion:nil];
}
}];
}
} #pragma mark - RPScreenRecorderDelegate
- (void)screenRecorder:(RPScreenRecorder *)screenRecorder didStopRecordingWithPreviewViewController:(RPPreviewViewController *)previewViewController error:(NSError *)error /*API_AVAILABLE(ios(11.0)*/{ if(@available(iOS 11.0,*)){
NSLog(@"didStopRecordingWithPreviewViewController: %@",error);
}
} -(void)screenRecorderDidChangeAvailability:(RPScreenRecorder *)screenRecorder{
NSLog(@"screenRecorderDidChangeAvailability:%@",screenRecorder);
} - (void)screenRecorder:(RPScreenRecorder *)screenRecorder didStopRecordingWithError:(NSError *)error previewViewController:(RPPreviewViewController *)previewViewController{
if(@available(iOS 9.0,*)){
NSLog(@"didStopRecordingWithError :%@",error);
}
} #pragma mark - RPPreviewViewControllerDelegate
- (void)previewControllerDidFinish:(RPPreviewViewController *)previewController{
NSLog(@"previewControllerDidFinish");
[previewController dismissViewControllerAnimated:YES completion:nil]; }
- (void)previewController:(RPPreviewViewController *)previewController didFinishWithActivityTypes:(NSSet<NSString *> *)activityTypes{
NSLog(@"didFinishWithActivityTypes:%@",activityTypes);
}
@end

App内部录屏直播

Bonjour

  • Bonjour 是 Apple 基于标准的网络技术,旨在帮助设备和服务在同一网络上发现彼此。例如,iPhone 和 iPad 设备使用 Bonjour 发现兼容“隔空打印”的打印机,iPhone 和 iPad 设备以及 Mac 电脑使用 Bonjour 发现兼容“隔空播放”的设备(如 Apple TV).

  • Bonjour

  • 由于bonjour服务是开源的,且iOS系统提供底层API库:DNS-SD,去实现此功能。

  • Bonjour服务一般用于发布服务全局广播,但如果服务不想被其它机器知道,只有制定机器知道,如何实现:

    • 1、客户端与服务器通信,等到服务器的服务ip地址,端口号
    • 2、客户端本地创建服务结点,并连接
  • 参考

  • 参考

APP广播端实现

- 被录制端需要在原有功能的基础上,增加一个唤起广播的入口。
- 点击直播会出现直播App选择(实现了ReplayKit Live的APP)
- ![](https://tva1.sinaimg.cn/large/008i3skNgy1gs7adt8fqij30u01szkjl.jpg)
//
// SystemScreenRecordController.m
// SLQDemo
//
// Created by song on 2022/01/6.
// Copyright 2022 了. All rights reserved.
// #import "SystemScreenRecordController.h"
#import <ReplayKit/ReplayKit.h> @interface SystemScreenRecordController ()<RPBroadcastActivityViewControllerDelegate,RPBroadcastControllerDelegate> @end @implementation SystemScreenRecordController - (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor greenColor];
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeSystem];
btn1.frame = CGRectMake(110, 100, 100, 33);
btn1.backgroundColor = [UIColor redColor];
[btn1 setTitle:@"点我啊" forState:UIControlStateNormal];
[btn1 addTarget:self action:@selector(systemBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn1]; }
- (void)systemBtnClick {
[self setupUI];
} - (void)setupUI { if (@available(iOS 10.0, *)) {
[RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithHandler:^(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error) {
if (error) {
NSLog(@"loadBroadcastActivityViewControllerWithHandler:%@",error);
}else{
broadcastActivityViewController.delegate = self;
broadcastActivityViewController.modalPresentationStyle = UIModalPresentationPopover;
[self presentViewController:broadcastActivityViewController animated:YES completion:nil];
}
}];
} else {
NSLog(@"不支持录制系统屏幕");
} }
#pragma mark - RPBroadcastActivityViewControllerDelegate
- (void)broadcastActivityViewController:(RPBroadcastActivityViewController *)broadcastActivityViewController didFinishWithBroadcastController:(RPBroadcastController *)broadcastController error:(NSError *)error{
NSLog(@"broadcastActivityViewController: didFinishWithBroadcastController:"); dispatch_async(dispatch_get_main_queue(), ^{
[broadcastActivityViewController dismissViewControllerAnimated:YES completion:nil];
}); NSLog(@"Boundle id :%@",broadcastController.broadcastURL); if (error) {
NSLog(@"BAC: %@ didFinishWBC: %@, err: %@",
broadcastActivityViewController,
broadcastController,
error);
return;
}
[broadcastController startBroadcastWithHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"startBroadcastWithHandler:%@",error);
}else{
NSLog(@"startBroadcast success");
}
}];
} - (void)broadcastController:(RPBroadcastController *)broadcastController didUpdateServiceInfo:(NSDictionary<NSString *,NSObject<NSCoding> *> *)serviceInfo{
NSLog(@"didUpdateServiceInfo:%@",serviceInfo);
} @end

广播端App(直播平台)的实现

  • 新增对 ReplayKit Live 的支持,只需要创建两个扩展的 target,分别是 Broadcast UI Extension 和 Broadcast Upload Extension
//
// SampleHandler.m
// broadcast
//
// Created by song on 2022/01/6.
// Copyright 2022 了. All rights reserved.
// #import "SampleHandler.h" @implementation SampleHandler - (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
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;
}
} @end
  • 实现录屏信息的界面,可以设置一下标题什么的
//
// BroadcastSetupViewController.m
// broadcastSetupUI
//
// Created by song on 2022/01/07.
// Copyright 2022 了. All rights reserved.
// #import "BroadcastSetupViewController.h" @implementation BroadcastSetupViewController - (void)viewDidLoad{
[super viewDidLoad];
NSLog(@"BroadcastSetupViewController");
self.view.backgroundColor = [UIColor redColor];
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeSystem];
btn1.frame = CGRectMake(110, 100, 200, 33);
btn1.backgroundColor = [UIColor redColor];
[btn1 setTitle:@"点我开始直播" forState:UIControlStateNormal];
[btn1 addTarget:self action:@selector(systemBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn1]; UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeSystem];
btn2.frame = CGRectMake(110, 200, 200, 33);
btn2.backgroundColor = [UIColor redColor];
[btn2 setTitle:@"取消直播" forState:UIControlStateNormal];
[btn2 addTarget:self action:@selector(stop) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn2]; }
- (void)systemBtnClick {
NSLog(@"开始直播");
[self userDidFinishSetup];
}
- (void)stop {
[self userDidCancelSetup];
}
// Call this method when the user has finished interacting with the view controller and a broadcast stream can start
- (void)userDidFinishSetup {
NSLog(@"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/test1"]; // Dictionary with setup information that will be provided to broadcast extension when broadcast is started
NSDictionary *setupInfo = @{ @"broadcastName" : @"App live" }; // 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
NSLog(@"userDidCancelSetup");
[self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"YourAppDomain" code:-1 userInfo:nil]];
} @end
  • 注意
iOS10只支持app内容录制,所以当app切到后台,录制内容将停止;
手机锁屏时,录制进程将停止;
这几个方法中的代码不能阻塞(例如写文件等慢操作),否则导致录制进程停止;

iOS12可在app里手动触发录屏

  • 在iOS 12.0+上出现了一个新的UI控件RPSystemBroadcastPickerView,用于展示用户启动系统录屏的指定视图.
  if (@available(iOS 12.0, *)) {
self.broadPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(110, 100, 100, 100)];
self.broadPickerView.preferredExtension = @"com.ask.answer.live.boradcastr";// nil的话列出所有可录屏的App
[self.view addSubview:self.broadPickerView];
}
  • 添加以上代码后,就会多出一个黑色按钮,点击就会弹出录制界面

录屏文件数据的共享

  • 每个Extension都需要一个宿主App,并且有自己的沙盒,当我们把录屏文件保存到沙盒中时宿主App是无法获取到的,那么只有采用共享的方式才能让宿主App拿到录屏文件。
  • App Group Share帮我们解决了这个问题,通过设置组间共享的模式,使得同一个Group下面的App可以共享资源,解决了沙盒的限制。

iOS14

  • 新增录制视频保存之URL的API,可直接保存到相册,保存到沙盒等
- (void)saveVideoWithUrl:(NSURL *)url {
PHPhotoLibrary *photoLibrary = [PHPhotoLibrary sharedPhotoLibrary];
[photoLibrary performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url]; } completionHandler:^(BOOL success, NSError * _Nullable error) {
if (success) {
NSLog(@"已将视频保存至相册");
} else {
NSLog(@"未能保存视频到相册");
}
}];
}
- (void)stop{
if ([RPScreenRecorder sharedRecorder].recording) { if (@available(iOS 14.0, *)) {
__weak typeof(self) weakSelf = self;
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES) firstObject];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/test.mp4",cachesDir]];
[[RPScreenRecorder sharedRecorder] stopRecordingWithOutputURL:url completionHandler:^(NSError * _Nullable error) {
NSLog(@"stopRecordingWithOutputURL:%@",url);
[weakSelf saveVideoWithUrl:url]; }];
} else {
[[RPScreenRecorder sharedRecorder] stopRecordingWithHandler:^(RPPreviewViewController * _Nullable previewViewController, NSError * _Nullable error) {
NSLog(@"stopRecordingWithHandler");
if (!error) {
previewViewController.previewControllerDelegate = self;
[self presentViewController:previewViewController animated:YES completion:nil];
}
}];
} }
}

保存视频到相册

  • 预览视频可通过AVPlayerViewController预览视频

  • 也可以直接保存到相册

  • SampleHandler数据流回调里处理视频

  • 通过AppGroup和宿主app共享数据

//
// SampleHandler.m
// broadcast
//
// Created by song on 2022/01/6.
// Copyright 2022 了. All rights reserved.
// #import "SampleHandler.h"
#import <AVFoundation/AVFoundation.h> @interface NSDate (Timestamp)
+ (NSString *)timestamp;
@end @implementation NSDate (Timestamp)
+ (NSString *)timestamp {
long long timeinterval = (long long)([NSDate timeIntervalSinceReferenceDate] * 1000);
return [NSString stringWithFormat:@"%lld", timeinterval];
}
@end @interface SampleHandler()
@property (nonatomic,strong) AVAssetWriter *assetWriter;
@property (nonatomic,strong) AVAssetWriterInput *videoInput;
@property (nonatomic,strong) AVAssetWriterInput *audioInput;
@end @implementation SampleHandler - (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
NSLog(@"启动广播:%@",setupInfo);
[self initData];
} - (NSString *)getDocumentPath { static NSString *replaysPath;
if (!replaysPath) {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentRootPath = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.com.ask.answer.live"];
replaysPath = [documentRootPath.path stringByAppendingPathComponent:@"Replays"];
if (![fileManager fileExistsAtPath:replaysPath]) {
NSError *error_createPath = nil;
BOOL success_createPath = [fileManager createDirectoryAtPath:replaysPath withIntermediateDirectories:true attributes:@{} error:&error_createPath];
if (success_createPath && !error_createPath) {
NSLog(@"%@路径创建成功!", replaysPath);
} else {
NSLog(@"%@路径创建失败:%@", replaysPath, error_createPath);
}
}else{
NSLog(@"%@路径已存在!", replaysPath);
}
}
return replaysPath;
}
- (NSURL *)getFilePathUrl {
NSString *time = [NSDate timestamp];
NSString *fileName = [time stringByAppendingPathExtension:@"mp4"];
NSString *fullPath = [[self getDocumentPath] stringByAppendingPathComponent:fileName];
return [NSURL fileURLWithPath:fullPath];
} - (NSArray <NSURL *> *)fetechAllResource {
NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *documentPath = [self getDocumentPath];
NSURL *documentURL = [NSURL fileURLWithPath:documentPath];
NSError *error = nil;
NSArray<NSURL *> *allResource = [fileManager contentsOfDirectoryAtURL:documentURL includingPropertiesForKeys:@[] options:(NSDirectoryEnumerationSkipsSubdirectoryDescendants) error:&error];
return allResource; }
- (void)initData {
if ([self.assetWriter canAddInput:self.videoInput]) {
[self.assetWriter addInput:self.videoInput];
}else{
NSLog(@"添加input失败");
}
}
- (AVAssetWriter *)assetWriter{
if (!_assetWriter) {
NSError *error = nil;
_assetWriter = [[AVAssetWriter alloc] initWithURL:[self getFilePathUrl] fileType:(AVFileTypeMPEG4) error:&error];
NSAssert(!error, @"_assetWriter 初始化失败");
}
return _assetWriter;
}
-(AVAssetWriterInput *)audioInput{
if (!_audioInput) {
// 音频参数
NSDictionary *audioCompressionSettings = @{
AVEncoderBitRatePerChannelKey:@(28000),
AVFormatIDKey:@(kAudioFormatMPEG4AAC),
AVNumberOfChannelsKey:@(1),
AVSampleRateKey:@(22050)
};
_audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioCompressionSettings];
}
return _audioInput;
} -(AVAssetWriterInput *)videoInput{
if (!_videoInput) { CGSize size = [UIScreen mainScreen].bounds.size;
// 视频大小
NSInteger numPixels = size.width * size.height;
// 像素比
CGFloat bitsPerPixel = 7.5;
NSInteger bitsPerSecond = numPixels * bitsPerPixel;
// 码率和帧率设置
NSDictionary *videoCompressionSettings = @{
AVVideoAverageBitRateKey:@(bitsPerSecond),//码率
AVVideoExpectedSourceFrameRateKey:@(25),// 帧率
AVVideoMaxKeyFrameIntervalKey:@(15),// 关键帧最大间隔
AVVideoProfileLevelKey:AVVideoProfileLevelH264BaselineAutoLevel,
AVVideoPixelAspectRatioKey:@{
AVVideoPixelAspectRatioVerticalSpacingKey:@(1),
AVVideoPixelAspectRatioHorizontalSpacingKey:@(1)
}
};
CGFloat scale = [UIScreen mainScreen].scale; // 视频参数
NSDictionary *videoOutputSettings = @{
AVVideoCodecKey:AVVideoCodecTypeH264,
AVVideoScalingModeKey:AVVideoScalingModeResizeAspectFill,
AVVideoWidthKey:@(size.width*scale),
AVVideoHeightKey:@(size.height*scale),
AVVideoCompressionPropertiesKey:videoCompressionSettings
}; _videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoOutputSettings];
_videoInput.expectsMediaDataInRealTime = true;
}
return _videoInput;
} - (void)broadcastPaused {
// User has requested to pause the broadcast. Samples will stop being delivered.
NSLog(@"暂停广播");
[self stopRecording];
} - (void)broadcastResumed {
// User has requested to resume the broadcast. Samples delivery will resume.
NSLog(@"恢复广播");
[self stopRecording];
} - (void)broadcastFinished {
// User has requested to finish the broadcast.
NSLog(@"完成广播");
[self stopRecording];
} - (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
switch (sampleBufferType) {
case RPSampleBufferTypeVideo:
// Handle video sample buffer
// 得到YUV数据
NSLog(@"视频流");
AVAssetWriterStatus status = self.assetWriter.status;
if (status == AVAssetWriterStatusFailed || status == AVAssetWriterStatusCompleted || status == AVAssetWriterStatusCancelled) {
return;
}
if (status == AVAssetWriterStatusUnknown) {
[self.assetWriter startWriting];
CMTime time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
[self.assetWriter startSessionAtSourceTime:time]; }
if (status == AVAssetWriterStatusWriting ) {
if (self.videoInput.isReadyForMoreMediaData) {
BOOL success = [self.videoInput appendSampleBuffer:sampleBuffer];
if (!success) {
[self stopRecording];
}
}
}
break;
case RPSampleBufferTypeAudioApp:
// Handle audio sample buffer for app audio
// 处理app音频
NSLog(@"App音频流");
break;
case RPSampleBufferTypeAudioMic:
// Handle audio sample buffer for mic audio
// 处理麦克风音频
NSLog(@"麦克风音频流");
if (self.audioInput.isReadyForMoreMediaData) {
BOOL success = [self.audioInput appendSampleBuffer:sampleBuffer];
if (!success) {
[self stopRecording];
}
}
break; default:
break;
}
}
- (void)stopRecording {
// if (self.assetWriter.status == AVAssetWriterStatusWriting) { [self.assetWriter finishWritingWithCompletionHandler:^{
NSLog(@"结束写入数据");
}];
// [self.audioInput markAsFinished];
// }
} @end
  • 预览视频
- (void)watchRecord:(UIButton *)sender {
NSLog(@"watchRecord");
NSArray<NSURL *> *allResource = [[self fetechAllResource] sortedArrayUsingComparator:^NSComparisonResult(NSURL * _Nonnull obj1, NSURL * _Nonnull obj2) {
//排序,每次都查看最新录制的视频
return [obj2.path compare:obj1.path options:(NSCaseInsensitiveSearch)];
}];
AVPlayerViewController *playerViewController;
playerViewController = [[AVPlayerViewController alloc] init];
NSLog(@"url%@:",allResource);
//
// for (NSURL *url in allResource) {
// [self saveVideoWithUrl:url];
// }
playerViewController.player = [AVPlayer playerWithURL:allResource.firstObject];
// playerViewController.delegate = self;
[self presentViewController:playerViewController animated:YES completion:^{
[playerViewController.player play];
NSLog(@"error == %@", playerViewController.player.error);
}]; }
- (NSString *)getDocumentPath { static NSString *replaysPath;
if (!replaysPath) {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentRootPath = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.com.ask.answer.live"];
replaysPath = [documentRootPath.path stringByAppendingPathComponent:@"Replays"];
if (![fileManager fileExistsAtPath:replaysPath]) {
NSError *error_createPath = nil;
BOOL success_createPath = [fileManager createDirectoryAtPath:replaysPath withIntermediateDirectories:true attributes:@{} error:&error_createPath];
if (success_createPath && !error_createPath) {
NSLog(@"%@路径创建成功!", replaysPath);
} else {
NSLog(@"%@路径创建失败:%@", replaysPath, error_createPath);
}
}
}
return replaysPath;
}
- (NSArray <NSURL *> *)fetechAllResource {
NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *documentPath = [self getDocumentPath];
NSURL *documentURL = [NSURL fileURLWithPath:documentPath];
NSError *error = nil;
NSArray<NSURL *> *allResource = [fileManager contentsOfDirectoryAtURL:documentURL includingPropertiesForKeys:@[] options:(NSDirectoryEnumerationSkipsSubdirectoryDescendants) error:&error];
return allResource; }
- (void)saveVideoWithUrl:(NSURL *)url {
PHPhotoLibrary *photoLibrary = [PHPhotoLibrary sharedPhotoLibrary];
[photoLibrary performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url]; } completionHandler:^(BOOL success, NSError * _Nullable error) {
if (success) {
NSLog(@"已将视频保存至相册");
} else {
NSLog(@"未能保存视频到相册");
}
}];
}

iOS 屏幕录制实现的更多相关文章

  1. iOS 屏幕录制功能

    系统框架 #import <ReplayKit/ReplayKit.h> // 系统视频录制框架 声明协议 <RPPreviewViewControllerDelegate> ...

  2. anyRTC iOS端屏幕录制开发指南

    一. 概述 实现直播过程中共享屏幕分为两个步骤:屏幕数据采集和流媒体数据推送.前对于 iOS 来说,屏幕采集需要系统的权限,受制于iOS系统的限制,第三方 app 并没有直接录制屏幕的权限,必须通过系 ...

  3. 屏幕录制H.264视频,AAC音频,MP4复,LibRTMP现场活动

    上周完成了一个屏幕录制节目,实时屏幕捕获.记录,视频H.264压缩,音频应用AAC压缩,复用MP4格公式,这使得计算机和ios设备上直接播放.支持HTML5的播放器都能够放,这是标准格式的优点.抓屏也 ...

  4. ios屏幕怎么投屏到电脑显示器

    iphone在国内一直都很受欢迎,为什么这么受欢迎呢?其实苹果手机操作系统非常的新颖,让人对手机有了重新的认识.但是ios屏幕怎么投屏到电脑显示器.感兴趣的一起阅读下面的内容吧! 使用工具: 苹果手机 ...

  5. X-Mirage苹果屏幕录制工具7天试用期破解 imsoft.cnblogs

    X-Mirage (PC) 能让你的 Windows 变成一个 iPhone.iPad 或者 iPod Touch 的屏幕镜像,应用程序.游戏.照片.视频等等一切可以在 iOS 移动端显示的东西,都镜 ...

  6. ffmpeg 屏幕录制 so easy....

    linux Linux下使用FFmpeg进行屏幕录制相对比较方便,可以使用x11grab,使用如下的命令: ffmpeg -f x11grab -s 1600x900 -r 50 -vcodec li ...

  7. iOS屏幕适配

    ## iOS屏幕适配 ### iOS屏幕适配发展史 1> iPhone4以前(没有iPad) * 不需要屏幕适配 2> iPad.iPhone5等设备出现 * 需要做横竖屏适配 * aut ...

  8. Mac与iPhone屏幕录制

    1. Mac电脑屏幕录制 1.1 文件->新建屏幕录制   1.2 点击红色按钮   1.3 截取需要录制的屏幕部分,点击开始录制   1.4 点击工具栏的停止按钮,停止录制   1.5 然后会 ...

  9. C# 与 Microsoft Expression Encoder实现屏幕录制

    在日常开发中,我们会经常遇到屏幕录制的需求.在C#中可以通过Expression Encoder的SDK实现这样的需求.首先需要下载Expression Encoder SDK,实现代码: priva ...

随机推荐

  1. Selenium_POM架构(17)

    POM是Page Object Model的简称,它是一种设计思想,意思是,把每一个页面,当做一个对象,页面的元素和元素之间操作方法就是页面对象的属性和行为. POM一般使用三层架构,分别为:基础封装 ...

  2. 05.python解析式与生成器表达式

    解析式和生成器表达式 列表解析式 列表解析式List Comprehension,也叫列表推导式 #生成一个列表,元素0-9,将每个元素加1后的平方值组成新的列表 x = [] for i in ra ...

  3. Java实现抽奖模块的相关分享

    Java实现抽奖模块的相关分享 最近进行的项目中,有个抽奖的需求,今天就把相关代码给大家分享一下. 一.DAO层 /** * 获取奖品列表 * @param systemVersion 手机系统版本( ...

  4. DEEP LEARNING WITH PYTORCH: A 60 MINUTE BLITZ | TENSORS

    Tensor是一种特殊的数据结构,非常类似于数组和矩阵.在PyTorch中,我们使用tensor编码模型的输入和输出,以及模型的参数. Tensor类似于Numpy的数组,除了tensor可以在GPU ...

  5. 【刷题-LeetCode】307. Range Sum Query - Mutable

    Range Sum Query - Mutable Given an integer array nums, find the sum of the elements between indices ...

  6. 事务与一致性:刚性or柔性

    转发自 https://cloud.tencent.com/developer/article/1038871 在高并发场景下,分布式储存和处理已经是常用手段.但分布式的结构势必会带来"不一 ...

  7. 【机器学习基础】无监督学习(1)——PCA

    前面对半监督学习部分作了简单的介绍,这里开始了解有关无监督学习的部分,无监督学习内容稍微较多,本节主要介绍无监督学习中的PCA降维的基本原理和实现. PCA 0.无监督学习简介 相较于有监督学习和半监 ...

  8. 关于python 爬虫遇到的反盗链

    首先声明:目标网址是从别人案例里得到的,内容你懂的... 本来闲来无事,学习下爬虫的知识,遇到恶心的反盗链,好在目标网址防盗链简单,代码里注明了如何去查看目标网址的防盗检查: 防盗链原理 http标准 ...

  9. 用python设计猜大小的游戏

    import random def roll_dice(numbers = 3,points = None): print("------摇骰子------") if points ...

  10. rsync实时备份监控命令(详细大全)

    目录 一:rsync介绍 1.rsync简介 2.rsync特性 3.rsync应用场景 4.rsync的传输方式 5.Rsync传输模式 二:RSYNC使用参数 三:参数使用案例 一:rsync介绍 ...