iOS 屏幕錄制實現


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(@"未能保存視頻到相冊");
        }
    }];
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM