iOS 螢幕錄製實現

struggle_time發表於2022-02-16

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(@"未能儲存視訊到相簿");
        }
    }];
}

相關文章