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服務是開源的,且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(@"未能儲存視訊到相簿");
}
}];
}