iOS 屏幕录制实现
目录
- iOS 屏幕录制实现
- 录屏API版本变化
- App内部录制屏幕
- 录音麦克风声音
- App内部录屏直播
- Bonjour
- APP广播端实现
- 广播端App(直播平台)的实现
- iOS12可在app里手动触发录屏
- 录屏文件数据的共享
- iOS14
- 保存视频到相册
录屏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
#import
#import "SystemScreenRecordController.h"
@interface MainViewController ()
@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 *)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 *)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
@interface SystemScreenRecordController ()
@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 *> *)serviceInfo{
NSLog(@"didUpdateServiceInfo:%@",serviceInfo);
}
@end
广播端App(直播平台)的实现
//
// SampleHandler.m
// broadcast
//
// Created by song on 2022/01/6.
// Copyright © 2022 了. All rights reserved.
//
#import "SampleHandler.h"
@implementation SampleHandler
- (void)broadcastStartedWithSetupInfo:(NSDictionary *)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
@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 *)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 *)fetechAllResource {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *documentPath = [self getDocumentPath];
NSURL *documentURL = [NSURL fileURLWithPath:documentPath];
NSError *error = nil;
NSArray *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 *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 *)fetechAllResource {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *documentPath = [self getDocumentPath];
NSURL *documentURL = [NSURL fileURLWithPath:documentPath];
NSError *error = nil;
NSArray *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(@"未能保存视频到相册");
}
}];
}
-
预览视频经常失败,重启手机后恢复正常,也许是API的问题。
-
参考1
-
参考2
-
参考3
-
AppGroup